UNPKG

2.8 kBJavaScriptView Raw
1import assign from 'object-assign'
2
3// Private action type for controlled-store
4const 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 */
23export 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}