1 | import { Action } from './types/actions'
|
---|
2 | import {
|
---|
3 | ActionFromReducersMapObject,
|
---|
4 | PreloadedStateShapeFromReducersMapObject,
|
---|
5 | Reducer,
|
---|
6 | StateFromReducersMapObject
|
---|
7 | } from './types/reducers'
|
---|
8 |
|
---|
9 | import ActionTypes from './utils/actionTypes'
|
---|
10 | import isPlainObject from './utils/isPlainObject'
|
---|
11 | import warning from './utils/warning'
|
---|
12 | import { kindOf } from './utils/kindOf'
|
---|
13 |
|
---|
14 | function 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 |
|
---|
62 | function 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 | */
|
---|
114 | export 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
|
---|
123 | export 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 | }
|
---|