Skip to content

Commit 06c6dff

Browse files
Make defaultState required when creating reducers (#127)
1 parent d8d7f63 commit 06c6dff

File tree

5 files changed

+107
-37
lines changed

5 files changed

+107
-37
lines changed

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ expect(actionThree(3)).to.deep.equal({
132132
});
133133
```
134134

135-
### `handleAction(type, reducer | reducerMap, ?defaultState)`
135+
### `handleAction(type, reducer | reducerMap, defaultState)`
136136

137137
```js
138138
import { handleAction } from 'redux-actions';
@@ -148,22 +148,22 @@ Otherwise, you can specify separate reducers for `next()` and `throw()`. This AP
148148
handleAction('FETCH_DATA', {
149149
next(state, action) {...},
150150
throw(state, action) {...}
151-
});
151+
}, defaultState);
152152
```
153153

154154
If either `next()` or `throw()` are `undefined` or `null`, then the identity function is used for that reducer.
155155

156-
The optional third parameter specifies a default or initial state, which is used when `undefined` is passed to the reducer.
156+
The third parameter `defaultState` is required, and is used when `undefined` is passed to the reducer.
157157

158-
### `handleActions(reducerMap, ?defaultState)`
158+
### `handleActions(reducerMap, defaultState)`
159159

160160
```js
161161
import { handleActions } from 'redux-actions';
162162
```
163163

164164
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).
165165

166-
The optional second parameter specifies a default or initial state, which is used when `undefined` is passed to the reducer.
166+
The second parameter `defaultState` is required, and is used when `undefined` is passed to the reducer.
167167

168168
(Internally, `handleActions()` works by applying multiple reducers in sequence using [reduce-reducers](https://github.com/acdlite/reduce-reducers).)
169169

src/__tests__/handleAction-test.js

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,21 @@ import { handleAction, createAction, createActions, combineActions } from '../';
55
describe('handleAction()', () => {
66
const type = 'TYPE';
77
const prevState = { counter: 3 };
8+
const defaultState = { counter: 0 };
89

910
describe('single handler form', () => {
11+
it('should throw an error if defaultState is not specified', () => {
12+
expect(() => {
13+
handleAction(type, undefined);
14+
}).to.throw(
15+
Error,
16+
'defaultState for reducer handling TYPE should be defined'
17+
);
18+
});
19+
1020
describe('resulting reducer', () => {
1121
it('returns previous state if type does not match', () => {
12-
const reducer = handleAction('NOTTYPE', () => null);
22+
const reducer = handleAction('NOTTYPE', () => null, defaultState);
1323
expect(reducer(prevState, { type })).to.equal(prevState);
1424
});
1525

@@ -24,7 +34,7 @@ describe('handleAction()', () => {
2434
it('accepts single function as handler', () => {
2535
const reducer = handleAction(type, (state, action) => ({
2636
counter: state.counter + action.payload
27-
}));
37+
}), defaultState);
2838
expect(reducer(prevState, { type, payload: 7 }))
2939
.to.deep.equal({
3040
counter: 10
@@ -35,15 +45,15 @@ describe('handleAction()', () => {
3545
const incrementAction = createAction(type);
3646
const reducer = handleAction(incrementAction, (state, action) => ({
3747
counter: state.counter + action.payload
38-
}));
48+
}), defaultState);
3949

4050
expect(reducer(prevState, incrementAction(7)))
4151
.to.deep.equal({
4252
counter: 10
4353
});
4454
});
4555

46-
it('accepts single function as handler and a default state', () => {
56+
it('accepts a default state used when the previous state is undefined', () => {
4757
const reducer = handleAction(type, (state, action) => ({
4858
counter: state.counter + action.payload
4959
}), { counter: 3 });
@@ -59,20 +69,30 @@ describe('handleAction()', () => {
5969

6070
const reducer = handleAction(increment, (state, { payload }) => ({
6171
counter: state.counter + payload
62-
}), { counter: 3 });
72+
}), defaultState);
6373

6474
expect(reducer(undefined, increment(7)))
6575
.to.deep.equal({
66-
counter: 10
76+
counter: 7
6777
});
6878
});
6979
});
7080
});
7181

7282
describe('map of handlers form', () => {
83+
it('should throw an error if defaultState is not specified', () => {
84+
expect(() => {
85+
handleAction(type, { next: () => null });
86+
})
87+
.to.throw(
88+
Error,
89+
'defaultState for reducer handling TYPE should be defined'
90+
);
91+
});
92+
7393
describe('resulting reducer', () => {
7494
it('returns previous state if type does not match', () => {
75-
const reducer = handleAction('NOTTYPE', { next: () => null });
95+
const reducer = handleAction('NOTTYPE', { next: () => null }, defaultState);
7696
expect(reducer(prevState, { type })).to.equal(prevState);
7797
});
7898

@@ -81,7 +101,7 @@ describe('handleAction()', () => {
81101
next: (state, action) => ({
82102
counter: state.counter + action.payload
83103
})
84-
});
104+
}, defaultState);
85105
expect(reducer(prevState, { type, payload: 7 }))
86106
.to.deep.equal({
87107
counter: 10
@@ -93,7 +113,7 @@ describe('handleAction()', () => {
93113
throw: (state, action) => ({
94114
counter: state.counter + action.payload
95115
})
96-
});
116+
}, defaultState);
97117

98118
expect(reducer(prevState, { type, payload: 7, error: true }))
99119
.to.deep.equal({
@@ -102,7 +122,7 @@ describe('handleAction()', () => {
102122
});
103123

104124
it('returns previous state if matching handler is not function', () => {
105-
const reducer = handleAction(type, { next: null, error: 123 });
125+
const reducer = handleAction(type, { next: null, error: 123 }, defaultState);
106126
expect(reducer(prevState, { type, payload: 123 })).to.equal(prevState);
107127
expect(reducer(prevState, { type, payload: 123, error: true }))
108128
.to.equal(prevState);
@@ -115,7 +135,8 @@ describe('handleAction()', () => {
115135
const action1 = createAction('ACTION_1');
116136
const reducer = handleAction(
117137
combineActions(action1, 'ACTION_2', 'ACTION_3'),
118-
(state, { payload }) => ({ ...state, number: state.number + payload })
138+
(state, { payload }) => ({ ...state, number: state.number + payload }),
139+
defaultState
119140
);
120141

121142
expect(reducer({ number: 1 }, action1(1))).to.deep.equal({ number: 2 });
@@ -129,7 +150,7 @@ describe('handleAction()', () => {
129150
next(state, { payload }) {
130151
return { ...state, number: state.number + payload };
131152
}
132-
});
153+
}, defaultState);
133154

134155
expect(reducer({ number: 1 }, action1(1))).to.deep.equal({ number: 2 });
135156
expect(reducer({ number: 1 }, { type: 'ACTION_2', payload: 2 })).to.deep.equal({ number: 3 });
@@ -146,7 +167,7 @@ describe('handleAction()', () => {
146167
throw(state) {
147168
return { ...state, threw: true };
148169
}
149-
});
170+
}, defaultState);
150171
const error = new Error;
151172

152173
expect(reducer({ number: 0 }, action1(error)))
@@ -161,6 +182,7 @@ describe('handleAction()', () => {
161182
const reducer = handleAction(
162183
combineActions('ACTION_1', 'ACTION_2'),
163184
(state, { payload }) => ({ ...state, state: state.number + payload }),
185+
defaultState
164186
);
165187

166188
const state = { number: 0 };
@@ -172,11 +194,11 @@ describe('handleAction()', () => {
172194
const reducer = handleAction(
173195
combineActions('INCREMENT', 'DECREMENT'),
174196
(state, { payload }) => ({ ...state, counter: state.counter + payload }),
175-
{ counter: 10 }
197+
defaultState
176198
);
177199

178-
expect(reducer(undefined, { type: 'INCREMENT', payload: +1 })).to.deep.equal({ counter: 11 });
179-
expect(reducer(undefined, { type: 'DECREMENT', payload: -1 })).to.deep.equal({ counter: 9 });
200+
expect(reducer(undefined, { type: 'INCREMENT', payload: +1 })).to.deep.equal({ counter: +1 });
201+
expect(reducer(undefined, { type: 'DECREMENT', payload: -1 })).to.deep.equal({ counter: -1 });
180202
});
181203

182204
it('should handle combined actions with symbols', () => {
@@ -185,7 +207,8 @@ describe('handleAction()', () => {
185207
const action3 = createAction(Symbol('ACTION_3'));
186208
const reducer = handleAction(
187209
combineActions(action1, action2, action3),
188-
(state, { payload }) => ({ ...state, number: state.number + payload })
210+
(state, { payload }) => ({ ...state, number: state.number + payload }),
211+
defaultState
189212
);
190213

191214
expect(reducer({ number: 0 }, action1(1)))
@@ -199,7 +222,7 @@ describe('handleAction()', () => {
199222

200223
describe('with invalid actions', () => {
201224
it('should throw a descriptive error when the action object is missing', () => {
202-
const reducer = handleAction(createAction('ACTION_1'), identity);
225+
const reducer = handleAction(createAction('ACTION_1'), identity, {});
203226
expect(
204227
() => reducer(undefined)
205228
).to.throw(
@@ -209,7 +232,7 @@ describe('handleAction()', () => {
209232
});
210233

211234
it('should throw a descriptive error when the action type is missing', () => {
212-
const reducer = handleAction(createAction('ACTION_1'), identity);
235+
const reducer = handleAction(createAction('ACTION_1'), identity, {});
213236
expect(
214237
() => reducer(undefined, {})
215238
).to.throw(
@@ -219,7 +242,7 @@ describe('handleAction()', () => {
219242
});
220243

221244
it('should throw a descriptive error when the action type is not a string or symbol', () => {
222-
const reducer = handleAction(createAction('ACTION_1'), identity);
245+
const reducer = handleAction(createAction('ACTION_1'), identity, {});
223246
expect(
224247
() => reducer(undefined, { type: false })
225248
).to.throw(

src/__tests__/handleActions-test.js

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,43 @@ import { expect } from 'chai';
22
import { handleActions, createAction, createActions, combineActions } from '../';
33

44
describe('handleActions', () => {
5+
const defaultState = { counter: 0 };
6+
7+
it('should throw an error when defaultState is not defined', () => {
8+
expect(() => {
9+
handleActions({
10+
INCREMENT: ({ counter }, { payload: amount }) => ({
11+
counter: counter + amount
12+
}),
13+
14+
DECREMENT: ({ counter }, { payload: amount }) => ({
15+
counter: counter - amount
16+
})
17+
});
18+
}).to.throw(
19+
Error,
20+
'defaultState for reducer handling INCREMENT should be defined'
21+
);
22+
});
23+
24+
it('should throw an error when defaultState is not defined for combinedActions', () => {
25+
expect(() => {
26+
handleActions({
27+
[
28+
combineActions(
29+
'INCREMENT',
30+
'DECREMENT'
31+
)
32+
]: ({ counter }, { type, payload: amount }) => ({
33+
counter: counter + (type === 'INCREMENT' ? +1 : -1) * amount
34+
})
35+
});
36+
}).to.throw(
37+
Error,
38+
'defaultState for reducer handling INCREMENT, DECREMENT should be defined'
39+
);
40+
});
41+
542
it('create a single handler from a map of multiple action handlers', () => {
643
const reducer = handleActions({
744
INCREMENT: ({ counter }, { payload: amount }) => ({
@@ -11,7 +48,7 @@ describe('handleActions', () => {
1148
DECREMENT: ({ counter }, { payload: amount }) => ({
1249
counter: counter - amount
1350
})
14-
});
51+
}, defaultState);
1552

1653
expect(reducer({ counter: 3 }, { type: 'INCREMENT', payload: 7 }))
1754
.to.deep.equal({
@@ -30,15 +67,15 @@ describe('handleActions', () => {
3067
[INCREMENT]: ({ counter }, { payload: amount }) => ({
3168
counter: counter + amount
3269
})
33-
});
70+
}, defaultState);
3471

3572
expect(reducer({ counter: 3 }, { type: INCREMENT, payload: 7 }))
3673
.to.deep.equal({
3774
counter: 10
3875
});
3976
});
4077

41-
it('accepts a default state as the second parameter', () => {
78+
it('accepts a default state used when previous state is undefined', () => {
4279
const reducer = handleActions({
4380
INCREMENT: ({ counter }, { payload: amount }) => ({
4481
counter: counter + amount
@@ -61,7 +98,7 @@ describe('handleActions', () => {
6198
[incrementAction]: ({ counter }, { payload: amount }) => ({
6299
counter: counter + amount
63100
})
64-
});
101+
}, defaultState);
65102

66103
expect(reducer({ counter: 3 }, incrementAction(7)))
67104
.to.deep.equal({
@@ -81,12 +118,12 @@ describe('handleActions', () => {
81118
[combineActions(increment, decrement)](state, { payload: { amount } }) {
82119
return { ...state, counter: state.counter + amount };
83120
}
84-
}, initialState);
121+
}, defaultState);
85122

86123
expect(reducer(initialState, increment(5))).to.deep.equal({ counter: 15 });
87124
expect(reducer(initialState, decrement(5))).to.deep.equal({ counter: 5 });
88125
expect(reducer(initialState, { type: 'NOT_TYPE', payload: 1000 })).to.equal(initialState);
89-
expect(reducer(undefined, increment(5))).to.deep.equal({ counter: 15 });
126+
expect(reducer(undefined, increment(5))).to.deep.equal({ counter: 5 });
90127
});
91128

92129
it('should accept combined actions as action types in the next/throw form', () => {
@@ -107,14 +144,14 @@ describe('handleActions', () => {
107144
return { ...state, counter: 0 };
108145
}
109146
}
110-
}, initialState);
147+
}, defaultState);
111148
const error = new Error;
112149

113150
// non-errors
114151
expect(reducer(initialState, increment(5))).to.deep.equal({ counter: 15 });
115152
expect(reducer(initialState, decrement(5))).to.deep.equal({ counter: 5 });
116153
expect(reducer(initialState, { type: 'NOT_TYPE', payload: 1000 })).to.equal(initialState);
117-
expect(reducer(undefined, increment(5))).to.deep.equal({ counter: 15 });
154+
expect(reducer(undefined, increment(5))).to.deep.equal({ counter: 5 });
118155

119156
// errors
120157
expect(
@@ -136,7 +173,7 @@ describe('handleActions', () => {
136173
[decrement]: ({ counter }, { payload }) => ({
137174
counter: counter - payload
138175
})
139-
});
176+
}, defaultState);
140177

141178
expect(reducer({ counter: 3 }, increment(2)))
142179
.to.deep.equal({

src/handleAction.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import isFunction from 'lodash/isFunction';
22
import identity from 'lodash/identity';
33
import isNil from 'lodash/isNil';
4+
import isUndefined from 'lodash/isUndefined';
45
import includes from 'lodash/includes';
56
import invariant from 'invariant';
67
import { isFSA } from 'flux-standard-action';
78
import { ACTION_TYPE_DELIMITER } from './combineActions';
89

910
export default function handleAction(actionType, reducers, defaultState) {
1011
const actionTypes = actionType.toString().split(ACTION_TYPE_DELIMITER);
12+
invariant(
13+
!isUndefined(defaultState),
14+
`defaultState for reducer handling ${actionTypes.join(', ')} should be defined`
15+
);
1116

1217
const [nextReducer, throwReducer] = isFunction(reducers)
1318
? [reducers, reducers]

src/handleActions.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@ import ownKeys from './ownKeys';
33
import reduceReducers from 'reduce-reducers';
44

55
export default function handleActions(handlers, defaultState) {
6-
const reducers = ownKeys(handlers).map(type => handleAction(type, handlers[type]));
6+
const reducers = ownKeys(handlers).map(type =>
7+
handleAction(
8+
type,
9+
handlers[type],
10+
defaultState
11+
)
12+
);
713
const reducer = reduceReducers(...reducers);
8-
9-
return (state = defaultState, action) => reducer(state, action);
14+
return (state, action) => reducer(state, action);
1015
}

0 commit comments

Comments
 (0)