diff --git a/src/utils/combineReducers.js b/src/utils/combineReducers.js index 540156f4f0..96412a460d 100644 --- a/src/utils/combineReducers.js +++ b/src/utils/combineReducers.js @@ -5,7 +5,7 @@ import pick from '../utils/pick'; /* eslint-disable no-console */ -function getErrorMessage(key, action) { +function getUndefinedStateErrorMessage(key, action) { var actionType = action && action.type; var actionName = actionType && `"${actionType.toString()}"` || 'an action'; @@ -15,28 +15,26 @@ function getErrorMessage(key, action) { ); } -function verifyStateShape(inputState, outputState, action) { +function getUnexpectedStateKeyWarningMessage(inputState, outputState, action) { var reducerKeys = Object.keys(outputState); var argumentName = action && action.type === ActionTypes.INIT ? 'initialState argument passed to createStore' : 'previous state received by the reducer'; if (reducerKeys.length === 0) { - console.error( + return ( 'Store does not have a valid reducer. Make sure the argument passed ' + 'to combineReducers is an object whose values are reducers.' ); - return; } if (!isPlainObject(inputState)) { - console.error( + return ( `The ${argumentName} has unexpected type of "` + ({}).toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] + `". Expected argument to be an object with the following ` + `keys: "${reducerKeys.join('", "')}"` ); - return; } var unexpectedKeys = Object.keys(inputState).filter( @@ -44,7 +42,7 @@ function verifyStateShape(inputState, outputState, action) { ); if (unexpectedKeys.length > 0) { - console.error( + return ( `Unexpected ${unexpectedKeys.length > 1 ? 'keys' : 'key'} ` + `"${unexpectedKeys.join('", "')}" found in ${argumentName}. ` + `Expected to find one of the known reducer keys instead: ` + @@ -53,6 +51,34 @@ function verifyStateShape(inputState, outputState, action) { } } +function assertReducerSanity(reducers) { + Object.keys(reducers).forEach(key => { + var reducer = reducers[key]; + var initialState = reducer(undefined, { type: ActionTypes.INIT }); + + 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.` + ); + } + + var type = '@@redux/PROBE_UNKNOWN_ACTION_' + Math.random().toString(36).substring(7).split('').join('.'); + if (typeof reducer(undefined, { type }) === 'undefined') { + 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, ` + + `in which case you must return the initial state, regardless of the ` + + `action type. The initial state may not be undefined.` + ); + } + }); +} + /** * Turns an object whose values are different reducer functions, into a single * reducer function. It will call every child reducer, and gather their results @@ -74,37 +100,11 @@ export default function combineReducers(reducers) { var finalReducers = pick(reducers, (val) => typeof val === 'function'); var sanityError; - Object.keys(finalReducers).forEach(key => { - var reducer = finalReducers[key]; - var initialState; - - try { - initialState = reducer(undefined, { type: ActionTypes.INIT }); - } catch (e) { - sanityError = e; - } - - if (!sanityError && typeof initialState === 'undefined') { - sanityError = 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.` - ); - } - - var type = '@@redux/PROBE_UNKNOWN_ACTION_' + Math.random().toString(36).substring(7).split('').join('.'); - if (!sanityError && typeof reducer(undefined, { type }) === 'undefined') { - sanityError = 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, ` + - `in which case you must return the initial state, regardless of the ` + - `action type. The initial state may not be undefined.` - ); - } - }); + try { + assertReducerSanity(finalReducers); + } catch (e) { + sanityError = e; + } var defaultState = mapValues(finalReducers, () => undefined); @@ -112,16 +112,21 @@ export default function combineReducers(reducers) { if (sanityError) { throw sanityError; } + var finalState = mapValues(finalReducers, (reducer, key) => { var newState = reducer(state[key], action); if (typeof newState === 'undefined') { - throw new Error(getErrorMessage(key, action)); + var errorMessage = getUndefinedStateErrorMessage(key, action); + throw new Error(errorMessage); } return newState; }); if (process.env.NODE_ENV !== 'production') { - verifyStateShape(state, finalState, action); + var warningMessage = getUnexpectedStateKeyWarningMessage(state, finalState, action); + if (warningMessage) { + console.error(warningMessage); + } } return finalState;