UNPKG

5.95 kBJavaScriptView Raw
1import ActionTypes from './utils/actionTypes'
2import warning from './utils/warning'
3import isPlainObject from './utils/isPlainObject'
4
5function getUndefinedStateErrorMessage(key, action) {
6 const actionType = action && action.type
7 const actionDescription =
8 (actionType && `action "${String(actionType)}"`) || 'an action'
9
10 return (
11 `Given ${actionDescription}, reducer "${key}" returned undefined. ` +
12 `To ignore an action, you must explicitly return the previous state. ` +
13 `If you want this reducer to hold no value, you can return null instead of undefined.`
14 )
15}
16
17function getUnexpectedStateShapeWarningMessage(
18 inputState,
19 reducers,
20 action,
21 unexpectedKeyCache
22) {
23 const reducerKeys = Object.keys(reducers)
24 const argumentName =
25 action && action.type === ActionTypes.INIT
26 ? 'preloadedState argument passed to createStore'
27 : 'previous state received by the reducer'
28
29 if (reducerKeys.length === 0) {
30 return (
31 'Store does not have a valid reducer. Make sure the argument passed ' +
32 'to combineReducers is an object whose values are reducers.'
33 )
34 }
35
36 if (!isPlainObject(inputState)) {
37 return (
38 `The ${argumentName} has unexpected type of "` +
39 {}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] +
40 `". Expected argument to be an object with the following ` +
41 `keys: "${reducerKeys.join('", "')}"`
42 )
43 }
44
45 const unexpectedKeys = Object.keys(inputState).filter(
46 key => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key]
47 )
48
49 unexpectedKeys.forEach(key => {
50 unexpectedKeyCache[key] = true
51 })
52
53 if (action && action.type === ActionTypes.REPLACE) return
54
55 if (unexpectedKeys.length > 0) {
56 return (
57 `Unexpected ${unexpectedKeys.length > 1 ? 'keys' : 'key'} ` +
58 `"${unexpectedKeys.join('", "')}" found in ${argumentName}. ` +
59 `Expected to find one of the known reducer keys instead: ` +
60 `"${reducerKeys.join('", "')}". Unexpected keys will be ignored.`
61 )
62 }
63}
64
65function assertReducerShape(reducers) {
66 Object.keys(reducers).forEach(key => {
67 const reducer = reducers[key]
68 const initialState = reducer(undefined, { type: ActionTypes.INIT })
69
70 if (typeof initialState === 'undefined') {
71 throw new Error(
72 `Reducer "${key}" returned undefined during initialization. ` +
73 `If the state passed to the reducer is undefined, you must ` +
74 `explicitly return the initial state. The initial state may ` +
75 `not be undefined. If you don't want to set a value for this reducer, ` +
76 `you can use null instead of undefined.`
77 )
78 }
79
80 if (
81 typeof reducer(undefined, {
82 type: ActionTypes.PROBE_UNKNOWN_ACTION()
83 }) === 'undefined'
84 ) {
85 throw new Error(
86 `Reducer "${key}" returned undefined when probed with a random type. ` +
87 `Don't try to handle ${
88 ActionTypes.INIT
89 } or other actions in "redux/*" ` +
90 `namespace. They are considered private. Instead, you must return the ` +
91 `current state for any unknown actions, unless it is undefined, ` +
92 `in which case you must return the initial state, regardless of the ` +
93 `action type. The initial state may not be undefined, but can be null.`
94 )
95 }
96 })
97}
98
99/**
100 * Turns an object whose values are different reducer functions, into a single
101 * reducer function. It will call every child reducer, and gather their results
102 * into a single state object, whose keys correspond to the keys of the passed
103 * reducer functions.
104 *
105 * @param {Object} reducers An object whose values correspond to different
106 * reducer functions that need to be combined into one. One handy way to obtain
107 * it is to use ES6 `import * as reducers` syntax. The reducers may never return
108 * undefined for any action. Instead, they should return their initial state
109 * if the state passed to them was undefined, and the current state for any
110 * unrecognized action.
111 *
112 * @returns {Function} A reducer function that invokes every reducer inside the
113 * passed object, and builds a state object with the same shape.
114 */
115export default function combineReducers(reducers) {
116 const reducerKeys = Object.keys(reducers)
117 const finalReducers = {}
118 for (let i = 0; i < reducerKeys.length; i++) {
119 const key = reducerKeys[i]
120
121 if (process.env.NODE_ENV !== 'production') {
122 if (typeof reducers[key] === 'undefined') {
123 warning(`No reducer provided for key "${key}"`)
124 }
125 }
126
127 if (typeof reducers[key] === 'function') {
128 finalReducers[key] = reducers[key]
129 }
130 }
131 const finalReducerKeys = Object.keys(finalReducers)
132
133 let unexpectedKeyCache
134 if (process.env.NODE_ENV !== 'production') {
135 unexpectedKeyCache = {}
136 }
137
138 let shapeAssertionError
139 try {
140 assertReducerShape(finalReducers)
141 } catch (e) {
142 shapeAssertionError = e
143 }
144
145 return function combination(state = {}, action) {
146 if (shapeAssertionError) {
147 throw shapeAssertionError
148 }
149
150 if (process.env.NODE_ENV !== 'production') {
151 const warningMessage = getUnexpectedStateShapeWarningMessage(
152 state,
153 finalReducers,
154 action,
155 unexpectedKeyCache
156 )
157 if (warningMessage) {
158 warning(warningMessage)
159 }
160 }
161
162 let hasChanged = false
163 const nextState = {}
164 for (let i = 0; i < finalReducerKeys.length; i++) {
165 const key = finalReducerKeys[i]
166 const reducer = finalReducers[key]
167 const previousStateForKey = state[key]
168 const nextStateForKey = reducer(previousStateForKey, action)
169 if (typeof nextStateForKey === 'undefined') {
170 const errorMessage = getUndefinedStateErrorMessage(key, action)
171 throw new Error(errorMessage)
172 }
173 nextState[key] = nextStateForKey
174 hasChanged = hasChanged || nextStateForKey !== previousStateForKey
175 }
176 return hasChanged ? nextState : state
177 }
178}