diff --git a/README.md b/README.md index 746ee3b8..a3efbb1c 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,7 @@ expect(actionThree(3)).to.deep.equal({ }); ``` -### `handleAction(type, reducer | reducerMap, ?defaultState)` +### `handleAction(type, reducer | reducerMap, defaultState)` ```js import { handleAction } from 'redux-actions'; @@ -148,14 +148,14 @@ Otherwise, you can specify separate reducers for `next()` and `throw()`. This AP handleAction('FETCH_DATA', { next(state, action) {...}, throw(state, action) {...} -}); +}, defaultState); ``` If either `next()` or `throw()` are `undefined` or `null`, then the identity function is used for that reducer. -The optional third parameter specifies a default or initial state, which is used when `undefined` is passed to the reducer. +The third parameter `defaultState` is required, and is used when `undefined` is passed to the reducer. -### `handleActions(reducerMap, ?defaultState)` +### `handleActions(reducerMap, defaultState)` ```js import { handleActions } from 'redux-actions'; @@ -163,7 +163,7 @@ import { handleActions } from 'redux-actions'; Creates multiple reducers using `handleAction()` and combines them into a single reducer that handles multiple actions. Accepts a map where the keys are passed as the first parameter to `handleAction()` (the action type), and the values are passed as the second parameter (either a reducer or reducer map). -The optional second parameter specifies a default or initial state, which is used when `undefined` is passed to the reducer. +The second parameter `defaultState` is required, and is used when `undefined` is passed to the reducer. (Internally, `handleActions()` works by applying multiple reducers in sequence using [reduce-reducers](https://github.com/acdlite/reduce-reducers).) diff --git a/src/__tests__/handleAction-test.js b/src/__tests__/handleAction-test.js index ff8f5018..a62d2e09 100644 --- a/src/__tests__/handleAction-test.js +++ b/src/__tests__/handleAction-test.js @@ -5,11 +5,21 @@ import { handleAction, createAction, createActions, combineActions } from '../'; describe('handleAction()', () => { const type = 'TYPE'; const prevState = { counter: 3 }; + const defaultState = { counter: 0 }; describe('single handler form', () => { + it('should throw an error if defaultState is not specified', () => { + expect(() => { + handleAction(type, undefined); + }).to.throw( + Error, + 'defaultState for reducer handling TYPE should be defined' + ); + }); + describe('resulting reducer', () => { it('returns previous state if type does not match', () => { - const reducer = handleAction('NOTTYPE', () => null); + const reducer = handleAction('NOTTYPE', () => null, defaultState); expect(reducer(prevState, { type })).to.equal(prevState); }); @@ -24,7 +34,7 @@ describe('handleAction()', () => { it('accepts single function as handler', () => { const reducer = handleAction(type, (state, action) => ({ counter: state.counter + action.payload - })); + }), defaultState); expect(reducer(prevState, { type, payload: 7 })) .to.deep.equal({ counter: 10 @@ -35,7 +45,7 @@ describe('handleAction()', () => { const incrementAction = createAction(type); const reducer = handleAction(incrementAction, (state, action) => ({ counter: state.counter + action.payload - })); + }), defaultState); expect(reducer(prevState, incrementAction(7))) .to.deep.equal({ @@ -43,7 +53,7 @@ describe('handleAction()', () => { }); }); - it('accepts single function as handler and a default state', () => { + it('accepts a default state used when the previous state is undefined', () => { const reducer = handleAction(type, (state, action) => ({ counter: state.counter + action.payload }), { counter: 3 }); @@ -59,20 +69,30 @@ describe('handleAction()', () => { const reducer = handleAction(increment, (state, { payload }) => ({ counter: state.counter + payload - }), { counter: 3 }); + }), defaultState); expect(reducer(undefined, increment(7))) .to.deep.equal({ - counter: 10 + counter: 7 }); }); }); }); describe('map of handlers form', () => { + it('should throw an error if defaultState is not specified', () => { + expect(() => { + handleAction(type, { next: () => null }); + }) + .to.throw( + Error, + 'defaultState for reducer handling TYPE should be defined' + ); + }); + describe('resulting reducer', () => { it('returns previous state if type does not match', () => { - const reducer = handleAction('NOTTYPE', { next: () => null }); + const reducer = handleAction('NOTTYPE', { next: () => null }, defaultState); expect(reducer(prevState, { type })).to.equal(prevState); }); @@ -81,7 +101,7 @@ describe('handleAction()', () => { next: (state, action) => ({ counter: state.counter + action.payload }) - }); + }, defaultState); expect(reducer(prevState, { type, payload: 7 })) .to.deep.equal({ counter: 10 @@ -93,7 +113,7 @@ describe('handleAction()', () => { throw: (state, action) => ({ counter: state.counter + action.payload }) - }); + }, defaultState); expect(reducer(prevState, { type, payload: 7, error: true })) .to.deep.equal({ @@ -102,7 +122,7 @@ describe('handleAction()', () => { }); it('returns previous state if matching handler is not function', () => { - const reducer = handleAction(type, { next: null, error: 123 }); + const reducer = handleAction(type, { next: null, error: 123 }, defaultState); expect(reducer(prevState, { type, payload: 123 })).to.equal(prevState); expect(reducer(prevState, { type, payload: 123, error: true })) .to.equal(prevState); @@ -115,7 +135,8 @@ describe('handleAction()', () => { const action1 = createAction('ACTION_1'); const reducer = handleAction( combineActions(action1, 'ACTION_2', 'ACTION_3'), - (state, { payload }) => ({ ...state, number: state.number + payload }) + (state, { payload }) => ({ ...state, number: state.number + payload }), + defaultState ); expect(reducer({ number: 1 }, action1(1))).to.deep.equal({ number: 2 }); @@ -129,7 +150,7 @@ describe('handleAction()', () => { next(state, { payload }) { return { ...state, number: state.number + payload }; } - }); + }, defaultState); expect(reducer({ number: 1 }, action1(1))).to.deep.equal({ number: 2 }); expect(reducer({ number: 1 }, { type: 'ACTION_2', payload: 2 })).to.deep.equal({ number: 3 }); @@ -146,7 +167,7 @@ describe('handleAction()', () => { throw(state) { return { ...state, threw: true }; } - }); + }, defaultState); const error = new Error; expect(reducer({ number: 0 }, action1(error))) @@ -161,6 +182,7 @@ describe('handleAction()', () => { const reducer = handleAction( combineActions('ACTION_1', 'ACTION_2'), (state, { payload }) => ({ ...state, state: state.number + payload }), + defaultState ); const state = { number: 0 }; @@ -172,11 +194,11 @@ describe('handleAction()', () => { const reducer = handleAction( combineActions('INCREMENT', 'DECREMENT'), (state, { payload }) => ({ ...state, counter: state.counter + payload }), - { counter: 10 } + defaultState ); - expect(reducer(undefined, { type: 'INCREMENT', payload: +1 })).to.deep.equal({ counter: 11 }); - expect(reducer(undefined, { type: 'DECREMENT', payload: -1 })).to.deep.equal({ counter: 9 }); + expect(reducer(undefined, { type: 'INCREMENT', payload: +1 })).to.deep.equal({ counter: +1 }); + expect(reducer(undefined, { type: 'DECREMENT', payload: -1 })).to.deep.equal({ counter: -1 }); }); it('should handle combined actions with symbols', () => { @@ -185,7 +207,8 @@ describe('handleAction()', () => { const action3 = createAction(Symbol('ACTION_3')); const reducer = handleAction( combineActions(action1, action2, action3), - (state, { payload }) => ({ ...state, number: state.number + payload }) + (state, { payload }) => ({ ...state, number: state.number + payload }), + defaultState ); expect(reducer({ number: 0 }, action1(1))) @@ -199,7 +222,7 @@ describe('handleAction()', () => { describe('with invalid actions', () => { it('should throw a descriptive error when the action object is missing', () => { - const reducer = handleAction(createAction('ACTION_1'), identity); + const reducer = handleAction(createAction('ACTION_1'), identity, {}); expect( () => reducer(undefined) ).to.throw( @@ -209,7 +232,7 @@ describe('handleAction()', () => { }); it('should throw a descriptive error when the action type is missing', () => { - const reducer = handleAction(createAction('ACTION_1'), identity); + const reducer = handleAction(createAction('ACTION_1'), identity, {}); expect( () => reducer(undefined, {}) ).to.throw( @@ -219,7 +242,7 @@ describe('handleAction()', () => { }); it('should throw a descriptive error when the action type is not a string or symbol', () => { - const reducer = handleAction(createAction('ACTION_1'), identity); + const reducer = handleAction(createAction('ACTION_1'), identity, {}); expect( () => reducer(undefined, { type: false }) ).to.throw( diff --git a/src/__tests__/handleActions-test.js b/src/__tests__/handleActions-test.js index 6335f2d2..3c6a6e08 100644 --- a/src/__tests__/handleActions-test.js +++ b/src/__tests__/handleActions-test.js @@ -2,6 +2,43 @@ import { expect } from 'chai'; import { handleActions, createAction, createActions, combineActions } from '../'; describe('handleActions', () => { + const defaultState = { counter: 0 }; + + it('should throw an error when defaultState is not defined', () => { + expect(() => { + handleActions({ + INCREMENT: ({ counter }, { payload: amount }) => ({ + counter: counter + amount + }), + + DECREMENT: ({ counter }, { payload: amount }) => ({ + counter: counter - amount + }) + }); + }).to.throw( + Error, + 'defaultState for reducer handling INCREMENT should be defined' + ); + }); + + it('should throw an error when defaultState is not defined for combinedActions', () => { + expect(() => { + handleActions({ + [ + combineActions( + 'INCREMENT', + 'DECREMENT' + ) + ]: ({ counter }, { type, payload: amount }) => ({ + counter: counter + (type === 'INCREMENT' ? +1 : -1) * amount + }) + }); + }).to.throw( + Error, + 'defaultState for reducer handling INCREMENT, DECREMENT should be defined' + ); + }); + it('create a single handler from a map of multiple action handlers', () => { const reducer = handleActions({ INCREMENT: ({ counter }, { payload: amount }) => ({ @@ -11,7 +48,7 @@ describe('handleActions', () => { DECREMENT: ({ counter }, { payload: amount }) => ({ counter: counter - amount }) - }); + }, defaultState); expect(reducer({ counter: 3 }, { type: 'INCREMENT', payload: 7 })) .to.deep.equal({ @@ -30,7 +67,7 @@ describe('handleActions', () => { [INCREMENT]: ({ counter }, { payload: amount }) => ({ counter: counter + amount }) - }); + }, defaultState); expect(reducer({ counter: 3 }, { type: INCREMENT, payload: 7 })) .to.deep.equal({ @@ -38,7 +75,7 @@ describe('handleActions', () => { }); }); - it('accepts a default state as the second parameter', () => { + it('accepts a default state used when previous state is undefined', () => { const reducer = handleActions({ INCREMENT: ({ counter }, { payload: amount }) => ({ counter: counter + amount @@ -61,7 +98,7 @@ describe('handleActions', () => { [incrementAction]: ({ counter }, { payload: amount }) => ({ counter: counter + amount }) - }); + }, defaultState); expect(reducer({ counter: 3 }, incrementAction(7))) .to.deep.equal({ @@ -81,12 +118,12 @@ describe('handleActions', () => { [combineActions(increment, decrement)](state, { payload: { amount } }) { return { ...state, counter: state.counter + amount }; } - }, initialState); + }, defaultState); expect(reducer(initialState, increment(5))).to.deep.equal({ counter: 15 }); expect(reducer(initialState, decrement(5))).to.deep.equal({ counter: 5 }); expect(reducer(initialState, { type: 'NOT_TYPE', payload: 1000 })).to.equal(initialState); - expect(reducer(undefined, increment(5))).to.deep.equal({ counter: 15 }); + expect(reducer(undefined, increment(5))).to.deep.equal({ counter: 5 }); }); it('should accept combined actions as action types in the next/throw form', () => { @@ -107,14 +144,14 @@ describe('handleActions', () => { return { ...state, counter: 0 }; } } - }, initialState); + }, defaultState); const error = new Error; // non-errors expect(reducer(initialState, increment(5))).to.deep.equal({ counter: 15 }); expect(reducer(initialState, decrement(5))).to.deep.equal({ counter: 5 }); expect(reducer(initialState, { type: 'NOT_TYPE', payload: 1000 })).to.equal(initialState); - expect(reducer(undefined, increment(5))).to.deep.equal({ counter: 15 }); + expect(reducer(undefined, increment(5))).to.deep.equal({ counter: 5 }); // errors expect( @@ -136,7 +173,7 @@ describe('handleActions', () => { [decrement]: ({ counter }, { payload }) => ({ counter: counter - payload }) - }); + }, defaultState); expect(reducer({ counter: 3 }, increment(2))) .to.deep.equal({ diff --git a/src/handleAction.js b/src/handleAction.js index ed9f56af..4c5231ba 100644 --- a/src/handleAction.js +++ b/src/handleAction.js @@ -1,6 +1,7 @@ import isFunction from 'lodash/isFunction'; import identity from 'lodash/identity'; import isNil from 'lodash/isNil'; +import isUndefined from 'lodash/isUndefined'; import includes from 'lodash/includes'; import invariant from 'invariant'; import { isFSA } from 'flux-standard-action'; @@ -8,6 +9,10 @@ import { ACTION_TYPE_DELIMITER } from './combineActions'; export default function handleAction(actionType, reducers, defaultState) { const actionTypes = actionType.toString().split(ACTION_TYPE_DELIMITER); + invariant( + !isUndefined(defaultState), + `defaultState for reducer handling ${actionTypes.join(', ')} should be defined` + ); const [nextReducer, throwReducer] = isFunction(reducers) ? [reducers, reducers] diff --git a/src/handleActions.js b/src/handleActions.js index 69589d02..69d7a83d 100644 --- a/src/handleActions.js +++ b/src/handleActions.js @@ -3,8 +3,13 @@ import ownKeys from './ownKeys'; import reduceReducers from 'reduce-reducers'; export default function handleActions(handlers, defaultState) { - const reducers = ownKeys(handlers).map(type => handleAction(type, handlers[type])); + const reducers = ownKeys(handlers).map(type => + handleAction( + type, + handlers[type], + defaultState + ) + ); const reducer = reduceReducers(...reducers); - - return (state = defaultState, action) => reducer(state, action); + return (state, action) => reducer(state, action); }