From de685de23289575f344638370912fea4989c0350 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Wed, 8 Jul 2015 13:32:48 +0300 Subject: [PATCH] Add more helpful error messages --- src/utils/composeReducers.js | 23 +++++++++++++-- test/composeReducers.spec.js | 54 ++++++++++++++++++++++++++++-------- 2 files changed, 63 insertions(+), 14 deletions(-) diff --git a/src/utils/composeReducers.js b/src/utils/composeReducers.js index e1f4249933..ab5ab7ce24 100644 --- a/src/utils/composeReducers.js +++ b/src/utils/composeReducers.js @@ -2,6 +2,25 @@ import mapValues from '../utils/mapValues'; import pick from '../utils/pick'; import invariant from 'invariant'; +function getErrorMessage(key, action) { + const actionType = action && action.type; + const actionName = actionType && `"${actionType}"` || 'an action'; + const reducerName = `Reducer "${key}"`; + + if (actionType === '@@INIT') { + return ( + `${reducerName} returned undefined during initialization. ` + + `If the state passed to the reducer is undefined, ` + + `you must explicitly return the initial state.` + ); + } + + return ( + `Reducer "${key}" returned undefined handling ${actionName}. ` + + `To ignore an action, you must explicitly return the previous state.` + ); +} + export default function composeReducers(reducers) { const finalReducers = pick(reducers, (val) => typeof val === 'function'); @@ -9,8 +28,8 @@ export default function composeReducers(reducers) { return mapValues(finalReducers, (reducer, key) => { const newState = reducer(state[key], action); invariant( - typeof newState !== 'undefined', - `Reducer ${key} returns undefined. By default reducer should return original state.` + typeof newState !== 'undefined', + getErrorMessage(key, action) ); return newState; }); diff --git a/test/composeReducers.spec.js b/test/composeReducers.spec.js index 0e578df68e..c2e429571f 100644 --- a/test/composeReducers.spec.js +++ b/test/composeReducers.spec.js @@ -30,29 +30,59 @@ describe('Utils', () => { ).toEqual(['stack']); }); - it('should throw an error if undefined return from reducer', () => { + it('should throw an error if a reducer returns undefined', () => { const reducer = composeReducers({ - stack: (state = []) => state, - bad: (state = [], action) => { - if (action.type === 'something') { + undefinedByDefault(state = 0, action) { + switch (action && action.type) { + case 'increment': + return state + 1; + case 'decrement': + return state - 1; + case '@@INIT': return state; + default: + return undefined; } } }); - expect(() => reducer({}, {type: '@@testType'})).toThrow(); + + const initialState = reducer(undefined, { type: '@@INIT' }); + expect( + () => reducer(initialState, { type: 'whatever' }) + ).toThrow( + /"undefinedByDefault".*"whatever"/ + ); + expect( + () => reducer(initialState, null) + ).toThrow( + /"undefinedByDefault".*an action/ + ); + expect( + () => reducer(initialState, {}) + ).toThrow( + /"undefinedByDefault".*an action/ + ); }); - - it('should throw an error if undefined return not by default', () => { + + it('should throw an error if a reducer returns undefined initializing', () => { const reducer = composeReducers({ - stack: (state = []) => state, - bad: (state = 1, action) => { - if (action.type !== 'something') { + undefinedInitially(state, action) { + switch (action.type) { + case 'increment': + return state + 1; + case 'decrement': + return state - 1; + default: return state; } } }); - expect(reducer({}, {type: '@@testType'})).toEqual({stack: [], bad: 1}); - expect(() => reducer({}, {type: 'something'})).toThrow(); + + expect( + () => reducer(undefined, { type: '@@INIT' }) + ).toThrow( + /"undefinedInitially".*initialization/ + ); }); }); });