├── .editorconfig ├── .eslintrc ├── .github └── FUNDING.yml ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── package.json ├── src ├── combineReducers.ts ├── index.ts └── utilities │ ├── getStateName.ts │ ├── getUnexpectedInvocationParameterMessage.ts │ ├── index.ts │ └── validateNextState.ts ├── tests ├── .eslintrc ├── combineReducers.ts └── utilities │ ├── getStateName.ts │ ├── getUnexpectedInvocationParameterMessage.ts │ └── validateNextState.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "canonical", 4 | "canonical/typescript" 5 | ], 6 | "parserOptions": { 7 | "project": "./tsconfig.json" 8 | }, 9 | "root": true, 10 | "rules": { 11 | "@typescript-eslint/no-explicit-any": 0, 12 | "@typescript-eslint/default-param-last": 0, 13 | "no-prototype-builtins": 0, 14 | "unicorn/no-object-as-default-parameter": 0, 15 | "@typescript-eslint/no-confusing-void-expression": 0 16 | } 17 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: gajus 2 | patreon: gajus 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | package-lock.json 4 | dist 5 | *.log 6 | .* 7 | !.gitignore 8 | !.npmignore 9 | !.babelrc 10 | !.travis.yml 11 | !.eslintrc 12 | !.editorconfig 13 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | tests 3 | coverage 4 | .* 5 | *.log 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | - 7 5 | - 6 6 | - 5 7 | notifications: 8 | email: false 9 | sudo: false 10 | script: 11 | - npm run test 12 | - npm run lint 13 | - npm run build 14 | after_success: 15 | - rm -fr ./dist 16 | - NODE_ENV=production npm run build 17 | - semantic-release pre && npm publish && semantic-release post 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Gajus Kuizinas (http://gajus.com/) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the Gajus Kuizinas (http://gajus.com/) nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL ANUARY BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `redux-immutable` 2 | 3 | [![GitSpo Mentions](https://gitspo.com/badges/mentions/gajus/redux-immutable?style=flat-square)](https://gitspo.com/mentions/gajus/redux-immutable) 4 | [![Travis build status](http://img.shields.io/travis/gajus/redux-immutable/master.svg?style=flat-square)](https://travis-ci.org/gajus/redux-immutable) 5 | [![NPM version](http://img.shields.io/npm/v/redux-immutable.svg?style=flat-square)](https://www.npmjs.org/package/redux-immutable) 6 | [![Canonical Code Style](https://img.shields.io/badge/code%20style-canonical-blue.svg?style=flat-square)](https://github.com/gajus/canonical) 7 | 8 | `redux-immutable` is used to create an equivalent function of Redux [`combineReducers`](http://redux.js.org/docs/api/combineReducers.html) that works with [Immutable.js](https://facebook.github.io/immutable-js/) state. 9 | 10 | When Redux [`createStore`](https://github.com/reactjs/redux/blob/master/docs/api/createStore.md) `reducer` is created using `redux-immutable` then `initialState` must be an instance of [`Immutable.Collection`](https://facebook.github.io/immutable-js/docs/#/Collection). 11 | 12 | ## Problem 13 | 14 | When [`createStore`](https://github.com/reactjs/redux/blob/v3.0.6/docs/api/createStore.md) is invoked with `initialState` that is an instance of `Immutable.Collection` further invocation of reducer will [produce an error](https://github.com/reactjs/redux/blob/v3.0.6/src/combineReducers.js#L31-L38): 15 | 16 | > The initialState argument passed to createStore has unexpected type of "Object". 17 | > Expected argument to be an object with the following keys: "data" 18 | 19 | This is because Redux `combineReducers` [treats `state` object as a plain JavaScript object](https://github.com/reactjs/redux/blob/v3.0.6/src/combineReducers.js#L120-L129). 20 | 21 | `combineReducers` created using `redux-immutable` uses Immutable.js API to iterate the state. 22 | 23 | ## Usage 24 | 25 | Create a store with `initialState` set to an instance of [`Immutable.Collection`](https://facebook.github.io/immutable-js/docs/#/Collection): 26 | 27 | ```js 28 | import { 29 | combineReducers 30 | } from 'redux-immutable'; 31 | 32 | import { 33 | createStore 34 | } from 'redux'; 35 | 36 | const initialState = Immutable.Map(); 37 | const rootReducer = combineReducers({}); 38 | const store = createStore(rootReducer, initialState); 39 | ``` 40 | 41 | 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: 42 | 43 | ```js 44 | const StateRecord = Immutable.Record({ 45 | foo: 'bar' 46 | }); 47 | const rootReducer = combineReducers({foo: fooReducer}, StateRecord); 48 | // rootReducer now has signature of rootReducer(state = StateRecord(), action) 49 | // state now must always have 'foo' property with 'bar' as its default value 50 | ``` 51 | 52 | When using `Immutable.Record` it is possible to delegate default values to child reducers: 53 | 54 | ```js 55 | const StateRecord = Immutable.Record({ 56 | foo: undefined 57 | }); 58 | const rootReducer = combineReducers({foo: fooReducer}, StateRecord); 59 | // state now must always have 'foo' property with its default value returned from fooReducer(undefined, action) 60 | ``` 61 | 62 | In general, `getDefaultState` function must return an instance of `Immutable.Record` or `Immutable.Collection` that implements `get`, `set` and `withMutations` methods. Such collections are `List`, `Map` and `OrderedMap`. 63 | 64 | ### Using with `react-router-redux` v4 and under 65 | 66 | `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: 67 | 68 | ```js 69 | import Immutable from 'immutable'; 70 | import { 71 | LOCATION_CHANGE 72 | } from 'react-router-redux'; 73 | 74 | const initialState = Immutable.fromJS({ 75 | locationBeforeTransitions: null 76 | }); 77 | 78 | export default (state = initialState, action) => { 79 | if (action.type === LOCATION_CHANGE) { 80 | return state.set('locationBeforeTransitions', action.payload); 81 | } 82 | 83 | return state; 84 | }; 85 | ``` 86 | 87 | Pass a selector to access the payload state and convert it to a JavaScript object via the [`selectLocationState` option on `syncHistoryWithStore`](https://github.com/reactjs/react-router-redux/tree/v4.0.2#history--synchistorywithstorehistory-store-options): 88 | 89 | ```js 90 | import { 91 | browserHistory 92 | } from 'react-router'; 93 | import { 94 | syncHistoryWithStore 95 | } from 'react-router-redux'; 96 | 97 | const history = syncHistoryWithStore(browserHistory, store, { 98 | selectLocationState (state) { 99 | return state.get('routing').toJS(); 100 | } 101 | }); 102 | ``` 103 | 104 | The `'routing'` path depends on the `rootReducer` definition. This example assumes that `routeReducer` is made available under `routing` property of the `rootReducer`. 105 | 106 | ### Using with `react-router-redux` v5 107 | To make [`react-router-redux` v5](https://github.com/ReactTraining/react-router/tree/master/packages/react-router-redux) work with Immutable.js you only need to use a custom reducer: 108 | 109 | ```js 110 | import { 111 | Map 112 | } from 'immutable'; 113 | import { 114 | LOCATION_CHANGE 115 | } from 'react-router-redux'; 116 | 117 | const initialState = Map({ 118 | location: null, 119 | action: null 120 | }); 121 | 122 | export function routerReducer(state = initialState, {type, payload = {}} = {}) { 123 | if (type === LOCATION_CHANGE) { 124 | const location = payload.location || payload; 125 | const action = payload.action; 126 | 127 | return state 128 | .set('location', location) 129 | .set('action', action); 130 | } 131 | 132 | return state; 133 | } 134 | 135 | ``` 136 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-immutable", 3 | "description": "redux-immutable is used to create an equivalent function of Redux combineReducers that works with Immutable.js state.", 4 | "main": "./dist/src/index.js", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/gajus/redux-immutable" 8 | }, 9 | "keywords": [ 10 | "immutable", 11 | "redux" 12 | ], 13 | "version": "1.0.0", 14 | "author": { 15 | "name": "Gajus Kuizinas", 16 | "email": "gajus@anuary.com", 17 | "url": "http://gajus.com" 18 | }, 19 | "license": "BSD-3-Clause", 20 | "peerDependencies": { 21 | "immutable": "^4.0.0" 22 | }, 23 | "devDependencies": { 24 | "@types/chai": "^4.3.0", 25 | "@types/mocha": "^9.1.0", 26 | "chai": "^3.5.0", 27 | "eslint": "^8.12.0", 28 | "eslint-config-canonical": "^33.0.1", 29 | "husky": "^0.12.0", 30 | "immutable": "^4.0.0", 31 | "mocha": "^3.2.0", 32 | "semantic-release": "^6.3.2", 33 | "ts-node": "^10.7.0", 34 | "typescript": "^4.6.3" 35 | }, 36 | "scripts": { 37 | "lint": "eslint ./src ./tests", 38 | "test": "mocha -r ts-node/register './tests/**/*.ts'", 39 | "build": "tsc", 40 | "pre-commit": "npm run lint && npm run test" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/combineReducers.ts: -------------------------------------------------------------------------------- 1 | import * as Immutable from 'immutable'; 2 | import { 3 | getUnexpectedInvocationParameterMessage, 4 | validateNextState, 5 | } from './utilities'; 6 | 7 | export const combineReducers = (reducers: any, getDefaultState = Immutable.Map): Function => { 8 | const reducerKeys = Object.keys(reducers); 9 | 10 | return (inputState = getDefaultState(), action: Object) => { 11 | // eslint-disable-next-line no-process-env 12 | if (process.env.NODE_ENV !== 'production') { 13 | const warningMessage = getUnexpectedInvocationParameterMessage(inputState, reducers, action); 14 | 15 | if (warningMessage) { 16 | // eslint-disable-next-line no-console 17 | console.error(warningMessage); 18 | } 19 | } 20 | 21 | return inputState 22 | .withMutations((temporaryState) => { 23 | for (const reducerName of reducerKeys) { 24 | const reducer = reducers[reducerName]; 25 | const currentDomainState = temporaryState.get(reducerName); 26 | const nextDomainState = reducer(currentDomainState, action); 27 | 28 | validateNextState(nextDomainState, reducerName, action); 29 | 30 | temporaryState.set(reducerName, nextDomainState); 31 | } 32 | }); 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | combineReducers, 3 | } from './combineReducers'; 4 | -------------------------------------------------------------------------------- /src/utilities/getStateName.ts: -------------------------------------------------------------------------------- 1 | export const getStateName = (action: any): string => { 2 | return action && action.type === '@@redux/INIT' ? 'initialState argument passed to createStore' : 'previous state received by the reducer'; 3 | }; 4 | -------------------------------------------------------------------------------- /src/utilities/getUnexpectedInvocationParameterMessage.ts: -------------------------------------------------------------------------------- 1 | import * as Immutable from 'immutable'; 2 | import { 3 | getStateName, 4 | } from './getStateName'; 5 | 6 | export const getUnexpectedInvocationParameterMessage = (state: any, reducers: any, action: any) => { 7 | const reducerNames = Object.keys(reducers); 8 | 9 | if (!reducerNames.length) { 10 | return 'Store does not have a valid reducer. Make sure the argument passed to combineReducers is an object whose values are reducers.'; 11 | } 12 | 13 | const stateName = getStateName(action); 14 | 15 | if (!Immutable.isCollection(state)) { 16 | return 'The ' + stateName + ' is of unexpected type. Expected argument to be an instance of Immutable.Collection or Immutable.Record with the following properties: "' + reducerNames.join('", "') + '".'; 17 | } 18 | 19 | const unexpectedStatePropertyNames = state.toSeq().keySeq().toArray().filter((name) => { 20 | return !reducers.hasOwnProperty(name); 21 | }); 22 | 23 | if (unexpectedStatePropertyNames.length > 0) { 24 | return 'Unexpected ' + (unexpectedStatePropertyNames.length === 1 ? 'property' : 'properties') + ' "' + unexpectedStatePropertyNames.join('", "') + '" found in ' + stateName + '. Expected to find one of the known reducer property names instead: "' + reducerNames.join('", "') + '". Unexpected properties will be ignored.'; 25 | } 26 | 27 | return null; 28 | }; 29 | -------------------------------------------------------------------------------- /src/utilities/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | getStateName, 3 | } from './getStateName'; 4 | export { 5 | getUnexpectedInvocationParameterMessage, 6 | } from './getUnexpectedInvocationParameterMessage'; 7 | export { 8 | validateNextState, 9 | } from './validateNextState'; 10 | -------------------------------------------------------------------------------- /src/utilities/validateNextState.ts: -------------------------------------------------------------------------------- 1 | export const validateNextState = (nextState: any, reducerName: string, action: any): void => { 2 | if (nextState === undefined) { 3 | throw new Error('Reducer "' + reducerName + '" returned undefined when handling "' + action.type + '" action. To ignore an action, you must explicitly return the previous state.'); 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "canonical/mocha" 3 | } 4 | -------------------------------------------------------------------------------- /tests/combineReducers.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-nested-callbacks */ 2 | 3 | import { 4 | expect, 5 | } from 'chai'; 6 | import * as Immutable from 'immutable'; 7 | import { 8 | combineReducers, 9 | } from '../src/combineReducers'; 10 | 11 | describe('combineReducers()', () => { 12 | context('reducer returns received state', () => { 13 | it('returns initial state', () => { 14 | const rootReducer = combineReducers({ 15 | foo: (state: any) => { 16 | return state; 17 | }, 18 | }); 19 | 20 | const initialState = Immutable.fromJS({ 21 | foo: { 22 | count: 0, 23 | }, 24 | }); 25 | 26 | expect(rootReducer(initialState, { 27 | type: 'ADD', 28 | })).to.equal(initialState); 29 | }); 30 | }); 31 | context('reducer creates new domain state', () => { 32 | it('returns new state', () => { 33 | const rootReducer = combineReducers({ 34 | foo: (state: any) => { 35 | return state.set('count', state.get('count') + 1); 36 | }, 37 | }); 38 | 39 | const initialState = Immutable.fromJS({ 40 | foo: { 41 | count: 0, 42 | }, 43 | }); 44 | 45 | expect(rootReducer(initialState, { 46 | type: 'ADD', 47 | }).getIn([ 48 | 'foo', 49 | 'count', 50 | ])).to.equal(1); 51 | }); 52 | }); 53 | context('root reducer is created from nested combineReducers', () => { 54 | it('returns initial state from default values', () => { 55 | const initialState = Immutable.fromJS({ 56 | outer: { 57 | inner: { 58 | bar: false, 59 | foo: true, 60 | }, 61 | }, 62 | }); 63 | 64 | const innerDefaultState = Immutable.fromJS({ 65 | bar: false, 66 | foo: true, 67 | }); 68 | 69 | const rootReducer = combineReducers({ 70 | outer: combineReducers({ 71 | inner: (state = innerDefaultState) => { 72 | return state; 73 | }, 74 | }), 75 | }); 76 | 77 | // eslint-disable-next-line no-undefined 78 | expect(rootReducer(undefined, {})).to.eql(initialState); 79 | }); 80 | }); 81 | context('root reducer uses a custom Immutable.Iterable as default state', () => { 82 | it('returns initial state as instance of supplied Immutable.Record', () => { 83 | const defaultRecord = Immutable.Record({ 84 | bar: { 85 | prop: 1, 86 | }, 87 | foo: undefined, // eslint-disable-line no-undefined 88 | }); 89 | const rootReducer = combineReducers({ 90 | bar: (state: any) => { 91 | return state; 92 | }, 93 | foo: (state = { 94 | count: 0, 95 | }) => { 96 | return state; 97 | }, 98 | }, defaultRecord as any); 99 | 100 | const initialState = { 101 | bar: { 102 | prop: 1, 103 | }, 104 | foo: { 105 | count: 0, 106 | }, 107 | }; 108 | 109 | // eslint-disable-next-line no-undefined 110 | const reducedState = rootReducer(undefined, {}); 111 | 112 | expect(reducedState.toJS()).to.deep.equal(initialState); 113 | expect(reducedState).to.be.instanceof(defaultRecord); 114 | }); 115 | it('returns initial state as instance of Immutable.OrderedMap', () => { 116 | const rootReducer = combineReducers({ 117 | bar: (state = { 118 | prop: 1, 119 | }) => { 120 | return state; 121 | }, 122 | foo: (state = { 123 | count: 0, 124 | }) => { 125 | return state; 126 | }, 127 | }, Immutable.OrderedMap as any); 128 | 129 | const initialState = { 130 | bar: { 131 | prop: 1, 132 | }, 133 | foo: { 134 | count: 0, 135 | }, 136 | }; 137 | 138 | // eslint-disable-next-line no-undefined 139 | const reducedState = rootReducer(undefined, {}); 140 | 141 | expect(reducedState.toJS()).to.deep.equal(initialState); 142 | expect(reducedState).to.be.instanceof(Immutable.OrderedMap); 143 | }); 144 | it('returns initial state as result of custom function call', () => { 145 | const getDefaultState = () => { 146 | return Immutable.Map({ 147 | bar: { 148 | prop: 1, 149 | }, 150 | }); 151 | }; 152 | 153 | const rootReducer = combineReducers({ 154 | bar: (state: any) => { 155 | return state; 156 | }, 157 | foo: (state = { 158 | count: 0, 159 | }) => { 160 | return state; 161 | }, 162 | }, getDefaultState as any); 163 | 164 | const initialState = { 165 | bar: { 166 | prop: 1, 167 | }, 168 | foo: { 169 | count: 0, 170 | }, 171 | }; 172 | 173 | // eslint-disable-next-line no-undefined 174 | const reducedState = rootReducer(undefined, {}); 175 | 176 | expect(reducedState.toJS()).to.deep.equal(initialState); 177 | expect(reducedState).to.be.instanceof(Immutable.Map); 178 | }); 179 | }); 180 | }); 181 | -------------------------------------------------------------------------------- /tests/utilities/getStateName.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-nested-callbacks */ 2 | 3 | import { 4 | expect, 5 | } from 'chai'; 6 | import { 7 | getStateName, 8 | } from '../../src/utilities/getStateName'; 9 | 10 | describe('utilities', () => { 11 | describe('getStateName()', () => { 12 | context('action.type is @@redux/INIT', () => { 13 | it('describes initialState', () => { 14 | const expectedStateName = getStateName({ 15 | type: '@@redux/INIT', 16 | }); 17 | 18 | expect(expectedStateName).to.equal('initialState argument passed to createStore'); 19 | }); 20 | }); 21 | context('action.type is anything else', () => { 22 | it('describes previous state', () => { 23 | const expectedStateName = getStateName({}); 24 | 25 | expect(expectedStateName).to.equal('previous state received by the reducer'); 26 | }); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/utilities/getUnexpectedInvocationParameterMessage.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-nested-callbacks */ 2 | 3 | import { 4 | expect, 5 | } from 'chai'; 6 | import * as Immutable from 'immutable'; 7 | import { 8 | getUnexpectedInvocationParameterMessage, 9 | } from '../../src/utilities/getUnexpectedInvocationParameterMessage'; 10 | 11 | describe('utilities', () => { 12 | describe('getUnexpectedInvocationParameterMessage()', () => { 13 | let validAction: any; 14 | let validReducers: any; 15 | let validState: any; 16 | 17 | beforeEach(() => { 18 | validState = Immutable.Map(); 19 | validReducers = { 20 | foo () {}, 21 | }; 22 | validAction = { 23 | type: '@@redux/INIT', 24 | }; 25 | }); 26 | 27 | context('store does not have a valid reducer', () => { 28 | it('returns an error', () => { 29 | const expectedErrorMessage = getUnexpectedInvocationParameterMessage(validState, {}, validAction); 30 | 31 | expect(expectedErrorMessage).to.equal('Store does not have a valid reducer. Make sure the argument passed to combineReducers is an object whose values are reducers.'); 32 | }); 33 | }); 34 | context('state is not an instance of Immutable.Collection or Immutable.Record', () => { 35 | it('returns error', () => { 36 | const expectedErrorMessage = getUnexpectedInvocationParameterMessage({}, validReducers, validAction); 37 | 38 | expect(expectedErrorMessage).to.equal('The initialState argument passed to createStore is of unexpected type. Expected argument to be an instance of Immutable.Collection or Immutable.Record with the following properties: "foo".'); 39 | }); 40 | }); 41 | context('state defines properties that are not present in the reducer map', () => { 42 | it('returns error', () => { 43 | const expectedErrorMessage = getUnexpectedInvocationParameterMessage(Immutable.Map({ 44 | bar: 'BAR', 45 | }), validReducers, validAction); 46 | 47 | expect(expectedErrorMessage).to.equal('Unexpected property "bar" found in initialState argument passed to createStore. Expected to find one of the known reducer property names instead: "foo". Unexpected properties will be ignored.'); 48 | }); 49 | }); 50 | context('valid', () => { 51 | it('returns null', () => { 52 | const expectedErrorMessage = getUnexpectedInvocationParameterMessage(validState, validReducers, validAction); 53 | 54 | expect(expectedErrorMessage).to.equal(null); 55 | }); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /tests/utilities/validateNextState.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-nested-callbacks */ 2 | 3 | import { 4 | expect, 5 | } from 'chai'; 6 | import * as Immutable from 'immutable'; 7 | import { 8 | validateNextState, 9 | } from '../../src/utilities/validateNextState'; 10 | 11 | describe('utilities', () => { 12 | describe('validateNextState()', () => { 13 | context('state is undefined', () => { 14 | it('throws an error', () => { 15 | expect(() => { 16 | // eslint-disable-next-line no-undefined 17 | validateNextState(undefined, 'reducer name', { 18 | type: 'foo', 19 | }); 20 | }).to.throw(Error, 'Reducer "reducer name" returned undefined when handling "foo" action. To ignore an action, you must explicitly return the previous state.'); 21 | }); 22 | }); 23 | context('state is defined', () => { 24 | it('returns undefined', () => { 25 | const result = validateNextState(Immutable.Map(), 'reducer name', { 26 | type: 'foo', 27 | }); 28 | 29 | // eslint-disable-next-line no-undefined 30 | expect(result).to.equal(undefined); 31 | }); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "outDir": "dist", 7 | "strict": true, 8 | "esModuleInterop": true 9 | }, 10 | "exclude": [ 11 | "node_modules", 12 | "dist" 13 | ] 14 | } --------------------------------------------------------------------------------