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