UNPKG

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