Skip to content

Commit 99a9cee

Browse files
committed
Merge pull request reduxjs#259 from gaearon/forbid-handling-private-actions
Handling private actions is an anti-pattern. Enforce it. (Fixes reduxjs#186)
2 parents afd4260 + 9045a0c commit 99a9cee

File tree

3 files changed

+68
-29
lines changed

3 files changed

+68
-29
lines changed

src/Store.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import invariant from 'invariant';
22
import isPlainObject from './utils/isPlainObject';
33

4+
// Don't ever try to handle these action types in your code. They are private.
5+
// For any unknown actions, you must return the current state.
6+
// If the current state is undefined, you must return the initial state.
7+
export const ActionTypes = {
8+
INIT: '@@redux/INIT'
9+
};
10+
411
export default class Store {
512
constructor(reducer, initialState) {
613
invariant(
@@ -19,7 +26,7 @@ export default class Store {
1926

2027
replaceReducer(nextReducer) {
2128
this.reducer = nextReducer;
22-
this.dispatch({ type: '@@INIT' });
29+
this.dispatch({ type: ActionTypes.INIT });
2330
}
2431

2532
dispatch(action) {

src/utils/combineReducers.js

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,11 @@
11
import mapValues from '../utils/mapValues';
22
import pick from '../utils/pick';
33
import invariant from 'invariant';
4+
import { ActionTypes } from '../Store';
45

56
function getErrorMessage(key, action) {
67
const actionType = action && action.type;
78
const actionName = actionType && `"${actionType}"` || 'an action';
8-
const reducerName = `Reducer "${key}"`;
9-
10-
if (actionType === '@@INIT') {
11-
return (
12-
`${reducerName} returned undefined during initialization. ` +
13-
`If the state passed to the reducer is undefined, ` +
14-
`you must explicitly return the initial state.`
15-
);
16-
}
179

1810
return (
1911
`Reducer "${key}" returned undefined handling ${actionName}. ` +
@@ -24,6 +16,28 @@ function getErrorMessage(key, action) {
2416
export default function combineReducers(reducers) {
2517
const finalReducers = pick(reducers, (val) => typeof val === 'function');
2618

19+
Object.keys(finalReducers).forEach(key => {
20+
const reducer = finalReducers[key];
21+
invariant(
22+
typeof reducer(undefined, { type: ActionTypes.INIT }) !== 'undefined',
23+
`Reducer "${key}" returned undefined during initialization. ` +
24+
`If the state passed to the reducer is undefined, you must ` +
25+
`explicitly return the initial state. The initial state may ` +
26+
`not be undefined.`
27+
);
28+
29+
const type = Math.random().toString(36).substring(7).split('').join('.');
30+
invariant(
31+
typeof reducer(undefined, { type }) !== 'undefined',
32+
`Reducer "${key}" returned undefined when probed with a random type. ` +
33+
`Don't try to handle ${ActionTypes.INIT} or other actions in "redux/*" ` +
34+
`namespace. They are considered private. Instead, you must return the ` +
35+
`current state for any unknown actions, unless it is undefined, ` +
36+
`in which case you must return the initial state, regardless of the ` +
37+
`action type. The initial state may not be undefined.`
38+
);
39+
});
40+
2741
return function composition(state = {}, action) {
2842
return mapValues(finalReducers, (reducer, key) => {
2943
const newState = reducer(state[key], action);

test/utils/combineReducers.spec.js

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import expect from 'expect';
22
import { combineReducers } from '../../src';
3+
import { ActionTypes } from '../../src/Store';
34

45
describe('Utils', () => {
56
describe('combineReducers', () => {
@@ -30,43 +31,44 @@ describe('Utils', () => {
3031
).toEqual(['stack']);
3132
});
3233

33-
it('should throw an error if a reducer returns undefined', () => {
34+
it('should throw an error if a reducer returns undefined handling an action', () => {
3435
const reducer = combineReducers({
35-
undefinedByDefault(state = 0, action) {
36+
counter(state = 0, action) {
3637
switch (action && action.type) {
3738
case 'increment':
3839
return state + 1;
3940
case 'decrement':
4041
return state - 1;
41-
case '@@INIT':
42-
return state;
43-
default:
42+
case 'whatever':
43+
case null:
44+
case undefined:
4445
return undefined;
46+
default:
47+
return state;
4548
}
4649
}
4750
});
4851

49-
const initialState = reducer(undefined, { type: '@@INIT' });
5052
expect(
51-
() => reducer(initialState, { type: 'whatever' })
53+
() => reducer({ counter: 0 }, { type: 'whatever' })
5254
).toThrow(
53-
/"undefinedByDefault".*"whatever"/
55+
/"counter".*"whatever"/
5456
);
5557
expect(
56-
() => reducer(initialState, null)
58+
() => reducer({ counter: 0 }, null)
5759
).toThrow(
58-
/"undefinedByDefault".*an action/
60+
/"counter".*an action/
5961
);
6062
expect(
61-
() => reducer(initialState, {})
63+
() => reducer({ counter: 0 }, {})
6264
).toThrow(
63-
/"undefinedByDefault".*an action/
65+
/"counter".*an action/
6466
);
6567
});
6668

6769
it('should throw an error if a reducer returns undefined initializing', () => {
68-
const reducer = combineReducers({
69-
undefinedInitially(state, action) {
70+
expect(() => combineReducers({
71+
counter(state, action) {
7072
switch (action.type) {
7173
case 'increment':
7274
return state + 1;
@@ -76,12 +78,28 @@ describe('Utils', () => {
7678
return state;
7779
}
7880
}
79-
});
81+
})).toThrow(
82+
/"counter".*initialization/
83+
);
84+
});
8085

81-
expect(
82-
() => reducer(undefined, { type: '@@INIT' })
83-
).toThrow(
84-
/"undefinedInitially".*initialization/
86+
it('should throw an error if a reducer attempts to handle a private action', () => {
87+
expect(() => combineReducers({
88+
counter(state, action) {
89+
switch (action.type) {
90+
case 'increment':
91+
return state + 1;
92+
case 'decrement':
93+
return state - 1;
94+
// Never do this in your code:
95+
case ActionTypes.INIT:
96+
return 0;
97+
default:
98+
return undefined;
99+
}
100+
}
101+
})).toThrow(
102+
/"counter".*private/
85103
);
86104
});
87105
});

0 commit comments

Comments
 (0)