1 | import ActionTypes from './utils/actionTypes'
|
2 | import warning from './utils/warning'
|
3 | import isPlainObject from './utils/isPlainObject'
|
4 |
|
5 | function 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 |
|
17 | function 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 |
|
65 | function 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 ${ActionTypes.INIT} or other actions in "redux/*" ` +
|
88 | `namespace. They are considered private. Instead, you must return the ` +
|
89 | `current state for any unknown actions, unless it is undefined, ` +
|
90 | `in which case you must return the initial state, regardless of the ` +
|
91 | `action type. The initial state may not be undefined, but can be null.`
|
92 | )
|
93 | }
|
94 | })
|
95 | }
|
96 |
|
97 |
|
98 |
|
99 |
|
100 |
|
101 |
|
102 |
|
103 |
|
104 |
|
105 |
|
106 |
|
107 |
|
108 |
|
109 |
|
110 |
|
111 |
|
112 |
|
113 | export default function combineReducers(reducers) {
|
114 | const reducerKeys = Object.keys(reducers)
|
115 | const finalReducers = {}
|
116 | for (let i = 0; i < reducerKeys.length; i++) {
|
117 | const key = reducerKeys[i]
|
118 |
|
119 | if (process.env.NODE_ENV !== 'production') {
|
120 | if (typeof reducers[key] === 'undefined') {
|
121 | warning(`No reducer provided for key "${key}"`)
|
122 | }
|
123 | }
|
124 |
|
125 | if (typeof reducers[key] === 'function') {
|
126 | finalReducers[key] = reducers[key]
|
127 | }
|
128 | }
|
129 | const finalReducerKeys = Object.keys(finalReducers)
|
130 |
|
131 |
|
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 | hasChanged =
|
177 | hasChanged || finalReducerKeys.length !== Object.keys(state).length
|
178 | return hasChanged ? nextState : state
|
179 | }
|
180 | }
|