1 | import assign from 'object-assign'
|
2 |
|
3 | // Private action type for controlled-store
|
4 | const ActionTypes = {
|
5 | UPDATE: '@@controlled/UPDATE'
|
6 | }
|
7 |
|
8 | /**
|
9 | * A Redux store enhancer that allows a redux app to operate as a controlled
|
10 | * component, selectively moving state out of the app and into a container.
|
11 | *
|
12 | * Enhances the store with an additional method `controlledUpdate()` that will
|
13 | * override the redux store state. Any keys on the state object passed to
|
14 | * `controlledUpdate()` will be "locked" in that any actions in the app will no
|
15 | * longer directly update that part of the state, but instead call the `onChange`
|
16 | * function passed to the constructor, with the state key that has been updated
|
17 | * and the new value.
|
18 | *
|
19 | * @param {Function} onChange
|
20 | * @param {Object} stateOverride
|
21 | * @return {Function} Redux Store Enhancer
|
22 | */
|
23 | export default (onChange, initialStateOverride = {}) => (createStore) => {
|
24 | // These properties of the app state are now controlled
|
25 | let controlledProps = Object.keys(initialStateOverride)
|
26 |
|
27 | return (reducer, initialState, enhancer) => {
|
28 | initialState = assign({}, initialState, initialStateOverride)
|
29 | // Create the store with an enhanced reducer
|
30 | const store = createStore(controlledReducer, initialState, enhancer)
|
31 |
|
32 | // Enhance the store with an additional method `controlledUpdate()`
|
33 | return assign({}, store, {
|
34 | controlledUpdate
|
35 | })
|
36 |
|
37 | function controlledReducer (state, action) {
|
38 | // Controlled updates skip app reducers and override the state
|
39 | if (action.type === ActionTypes.UPDATE) {
|
40 | return assign({}, state, action.payload)
|
41 | }
|
42 | let hasChanged = false
|
43 | const newState = reducer(state, action)
|
44 | Object.keys(newState).forEach(key => {
|
45 | if (newState[key] === state[key]) return
|
46 | const value = newState[key]
|
47 | process.nextTick(() => onChange(key, value))
|
48 | if (controlledProps.indexOf(key) > -1) {
|
49 | // If any controlled props of the state are updated, we hide the
|
50 | // initial change in state from the redux store and instead
|
51 | // call the `onChange` function with the key that has been updated
|
52 | // and the new value. Needs to run on nextTick to avoid `controlledUpdate()`
|
53 | // being called in the same tick and resulting in a `store.dispatch()`
|
54 | // inside this reducer.
|
55 | newState[key] = state[key]
|
56 | } else {
|
57 | // Unless an uncontrolled prop has been changed, we'll just return the existing state
|
58 | hasChanged = true
|
59 | }
|
60 | })
|
61 | return hasChanged ? newState : state
|
62 | }
|
63 |
|
64 | function controlledUpdate (stateOverride) {
|
65 | controlledProps = Object.keys(stateOverride)
|
66 | store.dispatch({
|
67 | type: ActionTypes.UPDATE,
|
68 | payload: stateOverride
|
69 | })
|
70 | }
|
71 | }
|
72 | }
|