You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
175 lines
5.9 KiB
175 lines
5.9 KiB
import ActionTypes from './utils/actionTypes' |
|
import warning from './utils/warning' |
|
import isPlainObject from './utils/isPlainObject' |
|
import { kindOf } from './utils/kindOf' |
|
|
|
function getUnexpectedStateShapeWarningMessage( |
|
inputState, |
|
reducers, |
|
action, |
|
unexpectedKeyCache |
|
) { |
|
const reducerKeys = Object.keys(reducers) |
|
const argumentName = |
|
action && action.type === ActionTypes.INIT |
|
? 'preloadedState argument passed to createStore' |
|
: 'previous state received by the reducer' |
|
|
|
if (reducerKeys.length === 0) { |
|
return ( |
|
'Store does not have a valid reducer. Make sure the argument passed ' + |
|
'to combineReducers is an object whose values are reducers.' |
|
) |
|
} |
|
|
|
if (!isPlainObject(inputState)) { |
|
return ( |
|
`The ${argumentName} has unexpected type of "${kindOf( |
|
inputState |
|
)}". Expected argument to be an object with the following ` + |
|
`keys: "${reducerKeys.join('", "')}"` |
|
) |
|
} |
|
|
|
const unexpectedKeys = Object.keys(inputState).filter( |
|
(key) => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key] |
|
) |
|
|
|
unexpectedKeys.forEach((key) => { |
|
unexpectedKeyCache[key] = true |
|
}) |
|
|
|
if (action && action.type === ActionTypes.REPLACE) return |
|
|
|
if (unexpectedKeys.length > 0) { |
|
return ( |
|
`Unexpected ${unexpectedKeys.length > 1 ? 'keys' : 'key'} ` + |
|
`"${unexpectedKeys.join('", "')}" found in ${argumentName}. ` + |
|
`Expected to find one of the known reducer keys instead: ` + |
|
`"${reducerKeys.join('", "')}". Unexpected keys will be ignored.` |
|
) |
|
} |
|
} |
|
|
|
function assertReducerShape(reducers) { |
|
Object.keys(reducers).forEach((key) => { |
|
const reducer = reducers[key] |
|
const initialState = reducer(undefined, { type: ActionTypes.INIT }) |
|
|
|
if (typeof initialState === 'undefined') { |
|
throw new Error( |
|
`The slice reducer for key "${key}" returned undefined during initialization. ` + |
|
`If the state passed to the reducer is undefined, you must ` + |
|
`explicitly return the initial state. The initial state may ` + |
|
`not be undefined. If you don't want to set a value for this reducer, ` + |
|
`you can use null instead of undefined.` |
|
) |
|
} |
|
|
|
if ( |
|
typeof reducer(undefined, { |
|
type: ActionTypes.PROBE_UNKNOWN_ACTION(), |
|
}) === 'undefined' |
|
) { |
|
throw new Error( |
|
`The slice reducer for key "${key}" returned undefined when probed with a random type. ` + |
|
`Don't try to handle '${ActionTypes.INIT}' or other actions in "redux/*" ` + |
|
`namespace. They are considered private. Instead, you must return the ` + |
|
`current state for any unknown actions, unless it is undefined, ` + |
|
`in which case you must return the initial state, regardless of the ` + |
|
`action type. The initial state may not be undefined, but can be null.` |
|
) |
|
} |
|
}) |
|
} |
|
|
|
/** |
|
* Turns an object whose values are different reducer functions, into a single |
|
* reducer function. It will call every child reducer, and gather their results |
|
* into a single state object, whose keys correspond to the keys of the passed |
|
* reducer functions. |
|
* |
|
* @param {Object} reducers An object whose values correspond to different |
|
* reducer functions that need to be combined into one. One handy way to obtain |
|
* it is to use ES6 `import * as reducers` syntax. The reducers may never return |
|
* undefined for any action. Instead, they should return their initial state |
|
* if the state passed to them was undefined, and the current state for any |
|
* unrecognized action. |
|
* |
|
* @returns {Function} A reducer function that invokes every reducer inside the |
|
* passed object, and builds a state object with the same shape. |
|
*/ |
|
export default function combineReducers(reducers) { |
|
const reducerKeys = Object.keys(reducers) |
|
const finalReducers = {} |
|
for (let i = 0; i < reducerKeys.length; i++) { |
|
const key = reducerKeys[i] |
|
|
|
if (process.env.NODE_ENV !== 'production') { |
|
if (typeof reducers[key] === 'undefined') { |
|
warning(`No reducer provided for key "${key}"`) |
|
} |
|
} |
|
|
|
if (typeof reducers[key] === 'function') { |
|
finalReducers[key] = reducers[key] |
|
} |
|
} |
|
const finalReducerKeys = Object.keys(finalReducers) |
|
|
|
// This is used to make sure we don't warn about the same |
|
// keys multiple times. |
|
let unexpectedKeyCache |
|
if (process.env.NODE_ENV !== 'production') { |
|
unexpectedKeyCache = {} |
|
} |
|
|
|
let shapeAssertionError |
|
try { |
|
assertReducerShape(finalReducers) |
|
} catch (e) { |
|
shapeAssertionError = e |
|
} |
|
|
|
return function combination(state = {}, action) { |
|
if (shapeAssertionError) { |
|
throw shapeAssertionError |
|
} |
|
|
|
if (process.env.NODE_ENV !== 'production') { |
|
const warningMessage = getUnexpectedStateShapeWarningMessage( |
|
state, |
|
finalReducers, |
|
action, |
|
unexpectedKeyCache |
|
) |
|
if (warningMessage) { |
|
warning(warningMessage) |
|
} |
|
} |
|
|
|
let hasChanged = false |
|
const nextState = {} |
|
for (let i = 0; i < finalReducerKeys.length; i++) { |
|
const key = finalReducerKeys[i] |
|
const reducer = finalReducers[key] |
|
const previousStateForKey = state[key] |
|
const nextStateForKey = reducer(previousStateForKey, action) |
|
if (typeof nextStateForKey === 'undefined') { |
|
const actionType = action && action.type |
|
throw new Error( |
|
`When called with an action of type ${ |
|
actionType ? `"${String(actionType)}"` : '(unknown type)' |
|
}, the slice reducer for key "${key}" returned undefined. ` + |
|
`To ignore an action, you must explicitly return the previous state. ` + |
|
`If you want this reducer to hold no value, you can return null instead of undefined.` |
|
) |
|
} |
|
nextState[key] = nextStateForKey |
|
hasChanged = hasChanged || nextStateForKey !== previousStateForKey |
|
} |
|
hasChanged = |
|
hasChanged || finalReducerKeys.length !== Object.keys(state).length |
|
return hasChanged ? nextState : state |
|
} |
|
}
|
|
|