[d24f17c] | 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 | }
|
---|