UNPKG

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