From 0c665c08c1b39a1817b2a3c8717aa5fec65b9901 Mon Sep 17 00:00:00 2001 From: Alan Jones Date: Wed, 25 May 2016 06:09:16 -0700 Subject: [PATCH 1/8] Allow reducers to consult entire global state --- src/combineReducers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/combineReducers.js b/src/combineReducers.js index 5a8a20a9b7..21a1f4be16 100644 --- a/src/combineReducers.js +++ b/src/combineReducers.js @@ -126,7 +126,7 @@ export default function combineReducers(reducers) { var key = finalReducerKeys[i] var reducer = finalReducers[key] var previousStateForKey = state[key] - var nextStateForKey = reducer(previousStateForKey, action) + var nextStateForKey = reducer(previousStateForKey, action, state) if (typeof nextStateForKey === 'undefined') { var errorMessage = getUndefinedStateErrorMessage(key, action) throw new Error(errorMessage) From 789bb8ce66c239cd82dc056c20fac63cbe05a71b Mon Sep 17 00:00:00 2001 From: Alan Jones Date: Wed, 25 May 2016 06:47:39 -0700 Subject: [PATCH 2/8] Pass next global state instead of previous --- src/combineReducers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/combineReducers.js b/src/combineReducers.js index 21a1f4be16..41e41eadcb 100644 --- a/src/combineReducers.js +++ b/src/combineReducers.js @@ -126,7 +126,7 @@ export default function combineReducers(reducers) { var key = finalReducerKeys[i] var reducer = finalReducers[key] var previousStateForKey = state[key] - var nextStateForKey = reducer(previousStateForKey, action, state) + var nextStateForKey = reducer(previousStateForKey, action, nextState) if (typeof nextStateForKey === 'undefined') { var errorMessage = getUndefinedStateErrorMessage(key, action) throw new Error(errorMessage) From d2a6a9fab09d9ebc7182a091c00cce34b7fb74b7 Mon Sep 17 00:00:00 2001 From: Alan Jones Date: Sun, 29 May 2016 06:37:00 -0700 Subject: [PATCH 3/8] Modify combineReducers to support multiple levels --- src/combineReducers.js | 200 +++++++++++++++++++++++++---------- test/combineReducers.spec.js | 140 ++++++++++++++++-------- 2 files changed, 235 insertions(+), 105 deletions(-) diff --git a/src/combineReducers.js b/src/combineReducers.js index 41e41eadcb..d06791a53e 100644 --- a/src/combineReducers.js +++ b/src/combineReducers.js @@ -33,6 +33,7 @@ function getUnexpectedStateShapeWarningMessage(inputState, reducers, action) { `keys: "${reducerKeys.join('", "')}"` ) } + var unexpectedKeys = Object.keys(inputState).filter(key => !reducers.hasOwnProperty(key)) @@ -46,32 +47,58 @@ function getUnexpectedStateShapeWarningMessage(inputState, reducers, action) { } } -function assertReducerSanity(reducers) { - Object.keys(reducers).forEach(key => { - var reducer = reducers[key] - var initialState = reducer(undefined, { type: ActionTypes.INIT }) - - if (typeof initialState === 'undefined') { - throw new Error( - `Reducer "${key}" returned undefined during initialization. ` + - `If the state passed to the reducer is undefined, you must ` + - `explicitly return the initial state. The initial state may ` + - `not be undefined.` - ) - } - var type = '@@redux/PROBE_UNKNOWN_ACTION_' + Math.random().toString(36).substring(7).split('').join('.') - if (typeof reducer(undefined, { type }) === 'undefined') { - throw new Error( - `Reducer "${key}" returned undefined when probed with a random type. ` + - `Don't try to handle ${ActionTypes.INIT} or other actions in "redux/*" ` + - `namespace. They are considered private. Instead, you must return the ` + - `current state for any unknown actions, unless it is undefined, ` + - `in which case you must return the initial state, regardless of the ` + - `action type. The initial state may not be undefined.` - ) - } - }) +function cleanReducerTree(rootReducerTree) { + + function _cleanReducerTree(reducerTree) { + let finalReducers = {} + + Object.keys(reducerTree).forEach(key => { + + if (reducerTree[key] && typeof reducerTree[key] == 'object') { + let nextLevelReducer = reducerTree[key] + + if (nextLevelReducer.length) { + throw new Error( + `Reducer object at "${key}" was empty. Every item in the ` + + `reducer tree must either be a function or a non-empty object` + ) + } + + finalReducers[key] = _cleanReducerTree(nextLevelReducer) + } else if (typeof reducerTree[key] == 'function') { + var reducer = reducerTree[key] + var initialState = reducer(undefined, { type: ActionTypes.INIT }) + + if (typeof initialState === 'undefined') { + throw new Error( + `Reducer "${key}" returned undefined during initialization. ` + + `If the state passed to the reducer is undefined, you must ` + + `explicitly return the initial state. The initial state may ` + + `not be undefined.` + ) + } + + var type = '@@redux/PROBE_UNKNOWN_ACTION_' + Math.random().toString(36).substring(7).split('').join('.') + if (typeof reducer(undefined, { type }) === 'undefined') { + throw new Error( + `Reducer "${key}" returned undefined when probed with a random type. ` + + `Don't try to handle ${ActionTypes.INIT} or other actions in "redux/*" ` + + `namespace. They are considered private. Instead, you must return the ` + + `current state for any unknown actions, unless it is undefined, ` + + `in which case you must return the initial state, regardless of the ` + + `action type. The initial state may not be undefined.` + ) + } + + finalReducers[key] = reducer + } + }) + + return finalReducers + } + + return _cleanReducerTree(rootReducerTree) } /** @@ -90,50 +117,107 @@ function assertReducerSanity(reducers) { * @returns {Function} A reducer function that invokes every reducer inside the * passed object, and builds a state object with the same shape. */ -export default function combineReducers(reducers) { - var reducerKeys = Object.keys(reducers) - var finalReducers = {} - for (var i = 0; i < reducerKeys.length; i++) { - var key = reducerKeys[i] - if (typeof reducers[key] === 'function') { - finalReducers[key] = reducers[key] - } - } - var finalReducerKeys = Object.keys(finalReducers) +//export default function combineReducers(reducers) { +// var reducerKeys = Object.keys(reducers) +// var finalReducers = {} +// +// for (var i = 0; i < reducerKeys.length; i++) { +// var key = reducerKeys[i] +// if (typeof reducers[key] === 'function') { +// finalReducers[key] = reducers[key] +// } +// } +// var finalReducerKeys = Object.keys(finalReducers) +// +// var sanityError +// try { +// assertReducerSanity(finalReducers) +// } catch (e) { +// sanityError = e +// } +// +// return function combination(state = {}, action) { +// if (sanityError) { +// throw sanityError +// } +// +// if (process.env.NODE_ENV !== 'production') { +// var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action) +// if (warningMessage) { +// warning(warningMessage) +// } +// } +// +// var hasChanged = false +// var nextState = {} +// for (var i = 0; i < finalReducerKeys.length; i++) { +// var key = finalReducerKeys[i] +// var reducer = finalReducers[key] +// var previousStateForKey = state[key] +// var nextStateForKey = reducer(previousStateForKey, action, nextState) +// +// if (typeof nextStateForKey === 'undefined') { +// var errorMessage = getUndefinedStateErrorMessage(key, action) +// throw new Error(errorMessage) +// } +// +// nextState[key] = nextStateForKey +// hasChanged = hasChanged || nextStateForKey !== previousStateForKey +// } +// return hasChanged ? nextState : state +// } +//} + + +export default function combineReducers(rootReducerTree) { + var finalReducers, sanityError - var sanityError try { - assertReducerSanity(finalReducers) + finalReducers = cleanReducerTree(rootReducerTree) } catch (e) { sanityError = e } - - return function combination(state = {}, action) { + + function reducer(topLevelState = {}, action) { if (sanityError) { throw sanityError } - if (process.env.NODE_ENV !== 'production') { - var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action) - if (warningMessage) { - warning(warningMessage) + function reduce(reducerTree, state) { + + if (process.env.NODE_ENV !== 'production') { + var warningMessage = getUnexpectedStateShapeWarningMessage(state, reducerTree, action) + if (warningMessage) { + warning(warningMessage) + } } - } - var hasChanged = false - var nextState = {} - for (var i = 0; i < finalReducerKeys.length; i++) { - var key = finalReducerKeys[i] - var reducer = finalReducers[key] - var previousStateForKey = state[key] - var nextStateForKey = reducer(previousStateForKey, action, nextState) - if (typeof nextStateForKey === 'undefined') { - var errorMessage = getUndefinedStateErrorMessage(key, action) - throw new Error(errorMessage) - } - nextState[key] = nextStateForKey - hasChanged = hasChanged || nextStateForKey !== previousStateForKey + let nextState = {} + let hasChanged = false + Object.keys(reducerTree).forEach((key) => { + let nextStateForKey, prevStateForKey = state[key] || {} + + if (typeof reducerTree[key] === 'object') { + nextStateForKey = reduce(reducerTree[key], prevStateForKey) + } else if (typeof reducerTree[key] === 'function') { + nextStateForKey = reducerTree[key](state[key], action, topLevelState) + } + + if (typeof nextStateForKey === 'undefined') { + var errorMessage = getUndefinedStateErrorMessage(key, action) + throw new Error(errorMessage) + } + + hasChanged = hasChanged || nextStateForKey !== prevStateForKey + nextState[key] = nextStateForKey + }) + + return hasChanged ? nextState : state } - return hasChanged ? nextState : state + + return reduce(finalReducers, topLevelState) } + + return reducer } + diff --git a/test/combineReducers.spec.js b/test/combineReducers.spec.js index d861ba371f..2a016cec9e 100644 --- a/test/combineReducers.spec.js +++ b/test/combineReducers.spec.js @@ -6,29 +6,55 @@ describe('Utils', () => { describe('combineReducers', () => { it('returns a composite reducer that maps the state keys to given reducers', () => { const reducer = combineReducers({ - counter: (state = 0, action) => - action.type === 'increment' ? state + 1 : state, + counters: { + foo: (state = 0, action) => + action.type === 'increment_foo' ? state + 1 : state, + bar: (state = 0, action) => + action.type === 'increment_bar' ? state + 1 : state + }, stack: (state = [], action) => - action.type === 'push' ? [ ...state, action.value ] : state + action.type === 'push' ? [ ...state, action.value ] : state }) - const s1 = reducer({}, { type: 'increment' }) - expect(s1).toEqual({ counter: 1, stack: [] }) - const s2 = reducer(s1, { type: 'push', value: 'a' }) - expect(s2).toEqual({ counter: 1, stack: [ 'a' ] }) + const s1 = reducer({}, { type: 'increment_foo' }) + expect(s1).toEqual({ counters: { foo: 1, bar: 0 }, stack: [] }) + + const s2 = reducer(s1, { type: 'increment_bar' }) + expect(s2).toEqual({ counters: { foo: 1, bar: 1 }, stack: [] }) + + const s3 = reducer(s2, { type: 'push', value: 'a' }) + expect(s3).toEqual({ counters: { foo: 1, bar: 1 }, stack: [ 'a' ] }) + }) + + it('passes the top-level state to each reducer in the tree', () => { + var reducers = { + foo: { + bar: () => { return {} } + } + } + + const spy = expect.spyOn(reducers.foo, 'bar').andCallThrough() + const reducer = combineReducers(reducers) + + reducer({ foo: { bar: 0 } }, { type: 'QUX' }) + var lastCall = spy.calls[spy.calls.length - 1] + expect(lastCall.arguments[0]).toEqual(0) + expect(lastCall.arguments[1]).toEqual({ type: 'QUX' }) + expect(lastCall.arguments[2]).toEqual({ foo: { bar: 0 } }) }) - it('ignores all props which are not a function', () => { + it('ignores all props which are not a function or a non-empty object', () => { const reducer = combineReducers({ fake: true, broken: 'string', - another: { nested: 'object' }, + counters: { increment: (state = []) => state }, stack: (state = []) => state }) - - expect( - Object.keys(reducer({ }, { type: 'push' })) - ).toEqual([ 'stack' ]) + + var stateKeys = Object.keys(reducer({ }, { type: 'push' })) + expect(stateKeys.length).toEqual(2) + expect(stateKeys).toInclude('counters') + expect(stateKeys).toInclude('stack') }) it('throws an error if a reducer returns undefined handling an action', () => { @@ -114,14 +140,16 @@ describe('Utils', () => { it('maintains referential equality if the reducers it is combining do', () => { const reducer = combineReducers({ - child1(state = { }) { - return state - }, - child2(state = { }) { - return state - }, - child3(state = { }) { - return state + foo: { + child1(state = { }) { + return state + }, + child2(state = { }) { + return state + }, + child3(state = { }) { + return state + } } }) @@ -131,24 +159,34 @@ describe('Utils', () => { it('does not have referential equality if one of the reducers changes something', () => { const reducer = combineReducers({ - child1(state = { }) { - return state - }, - child2(state = { count: 0 }, action) { - switch (action.type) { - case 'increment': - return { count: state.count + 1 } - default: - return state + foo: { + child1(state = { }) { + return state + }, + child2(state = { count: 0 }, action) { + switch (action.type) { + case 'increment': + return { count: state.count + 1 } + default: + return state + } + }, + child3(state = { }) { + return state } }, - child3(state = { }) { - return state + bar: { + child1(state = { }) { + return state + } } }) const initialState = reducer(undefined, '@@INIT') - expect(reducer(initialState, { type: 'increment' })).toNotBe(initialState) + const nextState = reducer(initialState, { type: 'increment' }) + expect(nextState).toNotBe(initialState) + expect(nextState.foo).toNotBe(initialState.foo) + expect(nextState.bar).toBe(initialState.bar) }) it('throws an error on first call if a reducer attempts to handle a private action', () => { @@ -185,54 +223,62 @@ describe('Utils', () => { it('warns if input state does not match reducer shape', () => { const spy = expect.spyOn(console, 'error') const reducer = combineReducers({ - foo(state = { bar: 1 }) { - return state - }, - baz(state = { qux: 3 }) { - return state + foo: { + bar(state = { baz: 1 }) { + return state + }, + qux(state = { corge: 3 }) { + return state + } } }) reducer() expect(spy.calls.length).toBe(0) - reducer({ foo: { bar: 2 } }) + reducer({ + foo : { + bar: { baz: 2 } + } + }) expect(spy.calls.length).toBe(0) reducer({ - foo: { bar: 2 }, - baz: { qux: 4 } + foo: { + bar: { baz: 2 }, + qux: { corge: 4 } + } }) expect(spy.calls.length).toBe(0) createStore(reducer, { bar: 2 }) expect(spy.calls[0].arguments[0]).toMatch( - /Unexpected key "bar".*createStore.*instead: "foo", "baz"/ + /Unexpected key "bar".*createStore.*instead: "foo"/ ) createStore(reducer, { bar: 2, qux: 4 }) expect(spy.calls[1].arguments[0]).toMatch( - /Unexpected keys "bar", "qux".*createStore.*instead: "foo", "baz"/ + /Unexpected keys "bar", "qux".*createStore.*instead: "foo"/ ) createStore(reducer, 1) expect(spy.calls[2].arguments[0]).toMatch( - /createStore has unexpected type of "Number".*keys: "foo", "baz"/ + /createStore has unexpected type of "Number".*keys: "foo"/ ) reducer({ bar: 2 }) expect(spy.calls[3].arguments[0]).toMatch( - /Unexpected key "bar".*reducer.*instead: "foo", "baz"/ + /Unexpected key "bar".*reducer.*instead: "foo"/ ) reducer({ bar: 2, qux: 4 }) expect(spy.calls[4].arguments[0]).toMatch( - /Unexpected keys "bar", "qux".*reducer.*instead: "foo", "baz"/ + /Unexpected keys "bar", "qux".*reducer.*instead: "foo"/ ) reducer(1) expect(spy.calls[5].arguments[0]).toMatch( - /reducer has unexpected type of "Number".*keys: "foo", "baz"/ + /reducer has unexpected type of "Number".*keys: "foo"/ ) spy.restore() From 309e579aafd4c38082c0c515633590383b7e5569 Mon Sep 17 00:00:00 2001 From: Alan Jones Date: Sun, 29 May 2016 06:38:41 -0700 Subject: [PATCH 4/8] Removing commented code --- src/combineReducers.js | 52 ------------------------------------------ 1 file changed, 52 deletions(-) diff --git a/src/combineReducers.js b/src/combineReducers.js index d06791a53e..353b2932cc 100644 --- a/src/combineReducers.js +++ b/src/combineReducers.js @@ -117,58 +117,6 @@ function cleanReducerTree(rootReducerTree) { * @returns {Function} A reducer function that invokes every reducer inside the * passed object, and builds a state object with the same shape. */ -//export default function combineReducers(reducers) { -// var reducerKeys = Object.keys(reducers) -// var finalReducers = {} -// -// for (var i = 0; i < reducerKeys.length; i++) { -// var key = reducerKeys[i] -// if (typeof reducers[key] === 'function') { -// finalReducers[key] = reducers[key] -// } -// } -// var finalReducerKeys = Object.keys(finalReducers) -// -// var sanityError -// try { -// assertReducerSanity(finalReducers) -// } catch (e) { -// sanityError = e -// } -// -// return function combination(state = {}, action) { -// if (sanityError) { -// throw sanityError -// } -// -// if (process.env.NODE_ENV !== 'production') { -// var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action) -// if (warningMessage) { -// warning(warningMessage) -// } -// } -// -// var hasChanged = false -// var nextState = {} -// for (var i = 0; i < finalReducerKeys.length; i++) { -// var key = finalReducerKeys[i] -// var reducer = finalReducers[key] -// var previousStateForKey = state[key] -// var nextStateForKey = reducer(previousStateForKey, action, nextState) -// -// if (typeof nextStateForKey === 'undefined') { -// var errorMessage = getUndefinedStateErrorMessage(key, action) -// throw new Error(errorMessage) -// } -// -// nextState[key] = nextStateForKey -// hasChanged = hasChanged || nextStateForKey !== previousStateForKey -// } -// return hasChanged ? nextState : state -// } -//} - - export default function combineReducers(rootReducerTree) { var finalReducers, sanityError From 77cf9dcbaf1102ad31e1f2134fff48721a771fd5 Mon Sep 17 00:00:00 2001 From: Alan Jones Date: Sun, 29 May 2016 06:54:06 -0700 Subject: [PATCH 5/8] Fixing JSDoc --- src/combineReducers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/combineReducers.js b/src/combineReducers.js index 353b2932cc..71abe10bbc 100644 --- a/src/combineReducers.js +++ b/src/combineReducers.js @@ -107,7 +107,7 @@ function cleanReducerTree(rootReducerTree) { * into a single state object, whose keys correspond to the keys of the passed * reducer functions. * - * @param {Object} reducers An object whose values correspond to different + * @param {Object} rootReducerTree An object whose values correspond to different * reducer functions that need to be combined into one. One handy way to obtain * it is to use ES6 `import * as reducers` syntax. The reducers may never return * undefined for any action. Instead, they should return their initial state From 1b8f1b9b914287caaa9ac2f4eb5c54546dac94ff Mon Sep 17 00:00:00 2001 From: Alan Jones Date: Mon, 30 May 2016 05:54:40 -0700 Subject: [PATCH 6/8] Allow combineReducers to be chainable --- src/combineReducers.js | 6 ++++-- test/combineReducers.spec.js | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/combineReducers.js b/src/combineReducers.js index 71abe10bbc..9180f1e5e6 100644 --- a/src/combineReducers.js +++ b/src/combineReducers.js @@ -126,7 +126,9 @@ export default function combineReducers(rootReducerTree) { sanityError = e } - function reducer(topLevelState = {}, action) { + function reducer(topLevelState = {}, action, stateWindow) { + stateWindow = stateWindow || topLevelState + if (sanityError) { throw sanityError } @@ -148,7 +150,7 @@ export default function combineReducers(rootReducerTree) { if (typeof reducerTree[key] === 'object') { nextStateForKey = reduce(reducerTree[key], prevStateForKey) } else if (typeof reducerTree[key] === 'function') { - nextStateForKey = reducerTree[key](state[key], action, topLevelState) + nextStateForKey = reducerTree[key](state[key], action, stateWindow) } if (typeof nextStateForKey === 'undefined') { diff --git a/test/combineReducers.spec.js b/test/combineReducers.spec.js index 2a016cec9e..c0925b0044 100644 --- a/test/combineReducers.spec.js +++ b/test/combineReducers.spec.js @@ -42,6 +42,24 @@ describe('Utils', () => { expect(lastCall.arguments[1]).toEqual({ type: 'QUX' }) expect(lastCall.arguments[2]).toEqual({ foo: { bar: 0 } }) }) + + it('can be chained and still correctly pass top-level state', () => { + var subreducers = { + bar : () => { return {} } + } + const spy = expect.spyOn(subreducers, 'bar').andCallThrough() + + var reducers = { + foo: combineReducers(subreducers) + } + + const rootReducer = combineReducers(reducers) + rootReducer({ foo: { bar: 0 } }, { type: 'QUX' }) + var lastCall = spy.calls[spy.calls.length - 1] + expect(lastCall.arguments[0]).toEqual(0) + expect(lastCall.arguments[1]).toEqual({ type: 'QUX' }) + expect(lastCall.arguments[2]).toEqual({ foo: { bar: 0 } }) + }) it('ignores all props which are not a function or a non-empty object', () => { const reducer = combineReducers({ From 9bd83637a031cc789e5cb651ded035627fb53a69 Mon Sep 17 00:00:00 2001 From: Alan Jones Date: Tue, 14 Jun 2016 21:23:03 -0700 Subject: [PATCH 7/8] Update combineReducers.md Updating combineReducers.md to reflect how `combineReducers` was expanded to support recursive reducer trees. --- docs/api/combineReducers.md | 66 ++++++++++++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 8 deletions(-) diff --git a/docs/api/combineReducers.md b/docs/api/combineReducers.md index 01492a128c..7ba0a7d3a0 100644 --- a/docs/api/combineReducers.md +++ b/docs/api/combineReducers.md @@ -4,34 +4,84 @@ As your app grows more complex, you’ll want to split your [reducing function]( The `combineReducers` helper function turns an object whose values are different reducing functions into a single reducing function you can pass to [`createStore`](createStore.md). -The resulting reducer calls every child reducer, and gathers their results into a single state object. **The shape of the state object matches the keys of the passed `reducers`**. +The resulting reducer calls every child reducer, and gathers their results into a single state object. **The shape of the state object will match the shape of the reducer tree `reducers`**. -Consequently, the state object will look like this: +In other words, if you create a reducer as such... + +``` +const myReducer = combineReducers({ + foo: , + bar: , + baz: { + baz1: , + baz2: + } +}) +``` + +...then the shape of the resulting state when running `myReducer` would look like the following. ``` { - reducer1: ... - reducer2: ... -} + foo: , + bar: , + baz: { + baz1: , + baz2: + } ``` You can control state key names by using different keys for the reducers in the passed object. For example, you may call `combineReducers({ todos: myTodosReducer, counter: myCounterReducer })` for the state shape to be `{ todos, counter }`. A popular convention is to name reducers after the state slices they manage, so you can use ES6 property shorthand notation: `combineReducers({ counter, todos })`. This is equivalent to writing `combineReducers({ counter: counter, todos: todos })`. + + > ##### A Note for Flux Users > This function helps you organize your reducers to manage their own slices of state, similar to how you would have different Flux Stores to manage different state. With Redux, there is just one store, but `combineReducers` helps you keep the same logical division between reducers. +#### + +Using `combineReducers` can be a good way to enforce separation of concerns in your reducer graph. Each sub-reducer receives the previous state for its specific reducer path and is only responsible for returning the next state for that same path. However, as your application becomes more sophisticated sometimes you'll have a need to allow a sub-reducer to access some part of the global state. This in particular can happen when you manage some global cached resource in your state that you don't want to duplicate for each sub-reducer that needs it (eg. when you use [Normalizr](https://github.com/paularmstrong/normalizr)). `combineReducers` handles this use case by allowing you each sub-reducer to optionally access the global state. + +For example, if you had a state shape like... + +``` +{ + myCache: { ... }, + foo: { ... }, + bar: { ... } +} +``` + +... you could access the state at `myCache` in other sub-reducers like so: + +``` +const reducer = combineReducers({ + myCache: , + foo: , + bar: (state, action, prevGlobalState) => { + let someObject = prevGlobalState.myCache['some_key'] + ... + } +}) +``` + +Note that because the order of object keys in javascript is undefined, the order in which your sub-reducers are executed is also undefined. As such, the `prevGlobalState` is exactly what it sounds like and does not reflect any changes in state caused during the processing of the current action. In addition, be sure not to mutate `prevGlobalState`. + + #### Arguments -1. `reducers` (*Object*): An object whose values correspond to different reducing functions that need to be combined into one. See the notes below for some rules every passed reducer must follow. +1. `reducers` (*Object*): An object tree that defines a potentially multi-level mapping of how the sub-reducer functions are composed into a final reducer function that creates the next state. Each value in this object must either be a sub-reducer function or another object that defines that next level of state. See the notes below for some rules every passed reducer must follow. > Earlier documentation suggested the use of the ES6 `import * as reducers` syntax to obtain the reducers object. This was the source of a lot of confusion, which is why we now recommend exporting a single reducer obtained using `combineReducers()` from `reducers/index.js` instead. An example is included below. #### Returns -(*Function*): A reducer that invokes every reducer inside the `reducers` object, and constructs a state object with the same shape. +(*Function*): A reducer that recursively invokes every reducer inside the `reducers` object, and constructs a state object with the same shape. + +In addition to the required `state` and `action` arguments, this reducer can also be passed a third argument `stateWindow`. When present, `stateWindow` is passed as the third argument to each of the sub-reducers when they are executed instead of `prevGlobalState`. This can be used as a way of restricting access to the global state from the sub-reducers or as a way to chain multiple calls to `combineReducers` while still giving the subsidiary reducing functions access to a specific slice of the state. #### Notes @@ -118,4 +168,4 @@ console.log(store.getState()) * This helper is just a convenience! You can write your own `combineReducers` that [works differently](https://github.com/acdlite/reduce-reducers), or even assemble the state object from the child reducers manually and write a root reducing function explicitly, like you would write any other function. -* You may call `combineReducers` at any level of the reducer hierarchy. It doesn’t have to happen at the top. In fact you may use it again to split the child reducers that get too complicated into independent grandchildren, and so on. +* You may call `combineReducers` at any level of the reducer hierarchy. Though it can compose a multi-level reducer tree, you don't always have to use it to define just the root reducer. You could, for instance, use it to create a root reducer with a custom sub-reducer that in turn, after executing some custom logic, invokes a reducer that was created by a separate call to `combineReducers`. From c56141ac931eb4aed2fbbb97ca48eb46ff92eaf1 Mon Sep 17 00:00:00 2001 From: Alan Jones Date: Tue, 14 Jun 2016 21:25:17 -0700 Subject: [PATCH 8/8] Update combineReducers.md --- docs/api/combineReducers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/combineReducers.md b/docs/api/combineReducers.md index 7ba0a7d3a0..24b14c078e 100644 --- a/docs/api/combineReducers.md +++ b/docs/api/combineReducers.md @@ -43,7 +43,7 @@ A popular convention is to name reducers after the state slices they manage, so #### -Using `combineReducers` can be a good way to enforce separation of concerns in your reducer graph. Each sub-reducer receives the previous state for its specific reducer path and is only responsible for returning the next state for that same path. However, as your application becomes more sophisticated sometimes you'll have a need to allow a sub-reducer to access some part of the global state. This in particular can happen when you manage some global cached resource in your state that you don't want to duplicate for each sub-reducer that needs it (eg. when you use [Normalizr](https://github.com/paularmstrong/normalizr)). `combineReducers` handles this use case by allowing you each sub-reducer to optionally access the global state. +Using `combineReducers` can be a good way to enforce separation of concerns in your reducer graph. Each sub-reducer receives the previous state for its specific reducer path and is only responsible for returning the next state for that same path. However, as your application becomes more sophisticated sometimes you'll need to cheat and allow a sub-reducer to access some part of the global state. This in particular can happen when you manage some global cached resource in your state that you don't want to duplicate for each sub-reducer that needs it (eg. when you use [Normalizr](https://github.com/paularmstrong/normalizr)). `combineReducers` handles this use case by allowing each sub-reducer to optionally access the global state if it happens to need it. For example, if you had a state shape like...