Skip to content

Instantly share code, notes, and snippets.

@gevgeny
Last active October 23, 2016 09:34
Show Gist options
  • Save gevgeny/d43766dbe585920b473e3bd2a95f6cdc to your computer and use it in GitHub Desktop.
Save gevgeny/d43766dbe585920b473e3bd2a95f6cdc to your computer and use it in GitHub Desktop.
One more try to implement react-redux app with fractal architecture to localize ui specific state
import { reducer as ui } from './ui'
const rootReducer = combineReducers({
ui
// other reducers
})
export default rootReducer
import { connectUi } from './ui'
import reducer, { increment } from './reducer'
export default const compose(
connectUi(
{ reducer, initialState: { value: 0 } },
({ value }) => ({ value }),
(dispatch, { uiKey }) => ({
onIncrement: item => dispatch(increment(uiKey))
})
)
)(({ name, value, onIncrement }) => (
<div>
<span>{value}</span>
<button onClick={onIncrement}></button>
</div>
))
import IncrementBox from './IncrementBox.jsx'
render(
<div>
<IncrementBox uiKey="foo"/>
<IncrementBox uiKey="bar"/>
<IncrementBox uiKey="baz"/>
</div>
, document.getElementById('root')
);
export default (state = {}, action) => {
switch (action.type) {
case 'ui/INCREMENT':
return { value: state.value + 1 }
default:
return state;
}
}
export const increment = (uiKey) => ({
type: 'ui/INCREMENT',
meta: { uiKey }
})
import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import toClass from 'recompose/toClass'
import compose from 'recompose/compose'
import reduce from 'lodash/reduce'
import uniqueId from 'lodash/uniqueId'
import omit from 'lodash/omit'
const MOUNT = 'ui/MOUN'
const UNMOUNT = 'ui/UNMOUNT'
const dynamicReducers = {}
// Static reducer which should be mounted to the store
// But composes dynamic list of reducers
// which are being added and removed during components lifecycle
export const reducer = (state = {}, action) => {
if (!action.type.startsWith('ui/')) {
return state
}
if (action.type === MOUNT) {
return {
...state,
[action.payload.key]: action.payload.initialState
}
} else if (action.type === UNMOUNT) {
return omit(state, action.payload)
} else if (action.meta && action.meta.uiKey) {
// Run specific reducer by provided key
const key = action.meta.uiKey
return {
...state,
[key]: dynamicReducers[key](state[key], action)
}
} else {
// Run all the UI reducers if the key is not provided
return reduce(dynamicReducers, (newState, reducer, key) => ({
...newState,
[key]: dynamicReducers[key](newState[key], action)
}), state)
}
}
export const connectUi = (
{ reducer, initialState },
mapStateToProps,
mapDispatchToProps
) => compose(
Target => class extends Component {
static contextTypes = {
store: PropTypes.shape({
dispatch: PropTypes.func.isRequired
})
}
static displayName = `ConnectUi(${Target.displayName || Target.name})`
componentWillMount() {
this.uiKey = this.props.uiKey || uniqueId()
if (dynamicReducers[this.uiKey]) {
throw new Error(`Component with key ${this.uiKey} already exists`)
}
dynamicReducers[this.uiKey] = reducer
this.context.store.dispatch({
type: MOUNT,
payload: { initialState, key: this.uiKey}
})
}
componentWillUnmount() {
delete dynamicReducers[this.uiKey]
this.context.store.dispatch({ type: UNMOUNT, payload : this.uiKey })
}
render() {
return <Target uiKey = {this.uiKey} {...this.props}/>
}
},
connect(
(state, { uiKey }) => state.ui[uiKey] || { },
mapDispatchToProps
)
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment