diff --git a/src/combineReducers.js b/src/combineReducers.js index 4929654193..15f42a7d52 100644 --- a/src/combineReducers.js +++ b/src/combineReducers.js @@ -57,19 +57,17 @@ function assertReducerSanity(reducers) { if (typeof initialState === 'undefined') { throw new Error( `Reducer "${key}" returned undefined during initialization. ` + - `If the state passed to the reducer is undefined, you must ` + - `explicitly return the initial state. The initial state may ` + - `not be undefined.` + `Reducers should never return undefined. Make sure this reducer ` + + `has a catch-all clause for unknown action types and that it returns a ` + + `default initial state if the state passed to it is undefined.` ) } - var type = '@@redux/PROBE_UNKNOWN_ACTION_' + Math.random().toString(36).substring(7).split('').join('.') - if (typeof reducer(undefined, { type }) === 'undefined') { + var randomState = Math.random().toString(36).substring(7).split('').join('.') + if (reducer(randomState, { type: ActionTypes.INIT }) !== randomState) { throw new Error( - `Reducer "${key}" returned undefined when probed with a random type. ` + - `Don't try to handle ${ActionTypes.INIT} or other actions in "redux/*" ` + - `namespace. They are considered private. Instead, you must return the ` + - `current state for any unknown actions, unless it is undefined, ` + + `Reducer "${key}" did not return the current state when probed with a random type. ` + + `You must return the current state for any unknown actions, unless it is undefined, ` + `in which case you must return the initial state, regardless of the ` + `action type. The initial state may not be undefined.` ) diff --git a/src/createStore.js b/src/createStore.js index b46ff9554b..86e425fd31 100644 --- a/src/createStore.js +++ b/src/createStore.js @@ -7,7 +7,7 @@ import isPlainObject from './utils/isPlainObject' * Do not reference these action types directly in your code. */ export var ActionTypes = { - INIT: '@@redux/INIT' + INIT: '@@redux/INIT_' + Math.random().toString(36).substring(7).split('').join('.') } /** diff --git a/test/combineReducers.spec.js b/test/combineReducers.spec.js index a2b00857d0..e3f4aac010 100644 --- a/test/combineReducers.spec.js +++ b/test/combineReducers.spec.js @@ -1,6 +1,6 @@ import expect from 'expect' import { combineReducers } from '../src' -import createStore, { ActionTypes } from '../src/createStore' +import createStore from '../src/createStore' describe('Utils', () => { describe('combineReducers', () => { @@ -84,6 +84,25 @@ describe('Utils', () => { ) }) + it('throws an error if reducer does not have a catch-all clause for unknown action types', () => { + const reducer = combineReducers({ + counter(state = 0, action) { + switch (action.type) { + case 'increment': + return state + 1 + case 'decrement': + return state - 1 + case undefined: + return state + } + } + }) + + expect(() => reducer()).toThrow( + /"counter".*initialization/ + ) + }) + it('catches error thrown in reducer when initializing and re-throw', () => { const reducer = combineReducers({ throwingReducer() { @@ -151,7 +170,7 @@ describe('Utils', () => { expect(reducer(initialState, { type: 'increment' })).toNotBe(initialState) }) - it('throws an error on first call if a reducer attempts to handle a private action', () => { + it('throws an error if reducer does not return current state for unknown action types', () => { const reducer = combineReducers({ counter(state, action) { switch (action.type) { @@ -159,16 +178,14 @@ describe('Utils', () => { return state + 1 case 'decrement': return state - 1 - // Never do this in your code: - case ActionTypes.INIT: - return 0 default: - return undefined + return 0 } } }) + expect(() => reducer()).toThrow( - /"counter".*private/ + /"counter".*probed/ ) })