From 50e365633a267bfce28765c21ff6dc544a23934b Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 14 Feb 2017 12:02:35 +0300 Subject: [PATCH] feat: allow to use Record and other Iterables as default state value (#58) * fix: correct test glob pattern * feat: allow using custom default state * test: add tests for custom default state * docs: update README with custom default state usage --- README.md | 23 ++++++++++ package.json | 2 +- src/combineReducers.js | 4 +- tests/combineReducers.js | 92 +++++++++++++++++++++++++++++++++++++++- 4 files changed, 117 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b7b5da7..e2dbc63 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,29 @@ const rootReducer = combineReducers({}); const store = createStore(rootReducer, initialState); ``` +By default, if `state` is `undefined`, `rootReducer(state, action)` is called with `state = Immutable.Map()`. A different default function can be provided as the second parameter to `combineReducers(reducers, getDefaultState)`, for example: + +```js +const StateRecord = Immutable.Record({ + foo: 'bar' +}); +const rootReducer = combineReducers({foo: fooReducer}, StateRecord); +// rootReducer now has signature of rootReducer(state = StateRecord(), action) +// state now must always have 'foo' property with 'bar' as its default value +``` + +When using `Immutable.Record` it is possible to delegate default values to child reducers: + +```js +const StateRecord = Immutable.Record({ + foo: undefined +}); +const rootReducer = combineReducers({foo: fooReducer}, StateRecord); +// state now must always have 'foo' property with its default value returned from fooReducer(undefined, action) +``` + +In general, `getDefaultState` function must return an instance of `Immutable.Iterable` that implements `get`, `set` and `withMutations` methods. Such iterables are `List`, `Map`, `OrderedMap` and `Record`. + ### Using with `react-router-redux` `react-router-redux` [`routeReducer`](https://github.com/reactjs/react-router-redux/tree/v4.0.2#routerreducer) does not work with Immutable.js. You need to use a custom reducer: diff --git a/package.json b/package.json index 74d04d4..06ff510 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ }, "scripts": { "lint": "eslint ./src ./tests", - "test": "mocha --compilers js:babel-register ./tests/**/*.js", + "test": "mocha --compilers js:babel-register './tests/**/*.js'", "build": "babel ./src --source-maps --out-dir ./dist", "benchmark": "NODE_ENV=production node ./benchmarks/index.js", "precommit": "npm run lint && npm run test" diff --git a/src/combineReducers.js b/src/combineReducers.js index fdf03b1..6369a64 100644 --- a/src/combineReducers.js +++ b/src/combineReducers.js @@ -4,11 +4,11 @@ import { validateNextState } from './utilities'; -export default (reducers: Object): Function => { +export default (reducers: Object, getDefaultState: ?Function = Immutable.Map): Function => { const reducerKeys = Object.keys(reducers); // eslint-disable-next-line space-infix-ops - return (inputState: ?Immutable.Map = Immutable.Map(), action: Object): Immutable.Map => { + return (inputState: ?Function = getDefaultState(), action: Object): Immutable.Map => { // eslint-disable-next-line no-process-env if (process.env.NODE_ENV !== 'production') { const warningMessage = getUnexpectedInvocationParameterMessage(inputState, reducers, action); diff --git a/tests/combineReducers.js b/tests/combineReducers.js index 4189a5d..cdebe20 100644 --- a/tests/combineReducers.js +++ b/tests/combineReducers.js @@ -65,8 +65,98 @@ describe('combineReducers()', () => { }) }); - // eslint-disable-next-line no-undefined + // eslint-disable-next-line no-undefined expect(rootReducer(undefined, {})).to.eql(initialState); }); }); + context('root reducer uses a custom Immutable.Iterable as default state', () => { + it('returns initial state as instance of supplied Immutable.Record', () => { + const defaultRecord = Immutable.Record({ + bar: { + prop: 1 + }, + foo: undefined // eslint-disable-line no-undefined + }); + const rootReducer = combineReducers({ + bar: (state) => { + return state; + }, + foo: (state = {count: 0}) => { + return state; + } + }, defaultRecord); + + const initialState = { + bar: { + prop: 1 + }, + foo: { + count: 0 + } + }; + + // eslint-disable-next-line no-undefined + const reducedState = rootReducer(undefined, {}); + + expect(reducedState.toJS()).to.deep.equal(initialState); + expect(reducedState).to.be.instanceof(defaultRecord); + }); + it('returns initial state as instance of Immutable.OrderedMap', () => { + const rootReducer = combineReducers({ + bar: (state = {prop: 1}) => { + return state; + }, + foo: (state = {count: 0}) => { + return state; + } + }, Immutable.OrderedMap); + + const initialState = { + bar: { + prop: 1 + }, + foo: { + count: 0 + } + }; + + // eslint-disable-next-line no-undefined + const reducedState = rootReducer(undefined, {}); + + expect(reducedState.toJS()).to.deep.equal(initialState); + expect(reducedState).to.be.instanceof(Immutable.OrderedMap); + }); + it('returns initial state as result of custom function call', () => { + const getDefaultState = () => { + return Immutable.Map({ + bar: { + prop: 1 + } + }); + }; + const rootReducer = combineReducers({ + bar: (state) => { + return state; + }, + foo: (state = {count: 0}) => { + return state; + } + }, getDefaultState); + + const initialState = { + bar: { + prop: 1 + }, + foo: { + count: 0 + } + }; + + // eslint-disable-next-line no-undefined + const reducedState = rootReducer(undefined, {}); + + expect(reducedState.toJS()).to.deep.equal(initialState); + expect(reducedState).to.be.instanceof(Immutable.Map); + }); + }); });