Skip to content

Commit

Permalink
Refactor warnings and errors in combineReducers()
Browse files Browse the repository at this point in the history
  • Loading branch information
gaearon committed Sep 26, 2015
1 parent 6fd9b05 commit a1485f0
Showing 1 changed file with 45 additions and 40 deletions.
85 changes: 45 additions & 40 deletions src/utils/combineReducers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -15,36 +15,34 @@ 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(
key => reducerKeys.indexOf(key) < 0
);

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: ` +
Expand All @@ -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
Expand All @@ -74,54 +100,33 @@ 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);

return function combination(state = defaultState, action) {
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;
Expand Down

0 comments on commit a1485f0

Please sign in to comment.