Добро пожаловать в чудный мир эльфов и волшебства javascript'а. В одной из прошлых статей мы рассматривали middleware функции. Там, заглянув в исходный код библиотеки Redux, было выяснено, что в её API входит всёго лишь 5 функций: createStore, combineReducers, applyMiddleware, bindActionCreators, compose.
Сегодня попробуем почитать исходный код и разобраться в ещё одной функции из API, а именно в функции combineReducers. Поехали знакомиться.

Что за функция combineReducers и зачем она нужна и кто вообще такие reducer'ы?
Reducer - термин, который привносит с собой redux. Reducer - функция, которая отвечает за формирование нового state приложения из исходного и объекта события. Функцию Reducer'а можно понять из следующей схемы.

Ниже пример reducer'а:
export default function user(state = initialState, action) {
switch (action.type) {
case USER_INFO_REQUEST: {
return Object.assign({}, state, {fetchingInfo: true});
}
case USER_INFO_COMPLETE: {
return fetchingUserInfoComplete(state, action);
}
case USER_INFO_FAIL: {
return Object.assign({}, state, {
fetchingInfo: false,
fetchInfoError: action.error,
});
}
}
Как видно из схемы и кода, action - объект, который, как правило, состоит из типа события и полезной нагрузки, которая отличается в зависимости от события. Reducer это яркий пример switch-case технологии. Очевидно, что с ростом количества возможных событий в системе функция будет разрарастаться в размерах. А блок обработки каждого события, возможно будет лезть 'в глубь' состояния, начиная от его корня, что не очень удобно, т.к. код становится избыточен. Хотелось бы провести декомпозицию и разбить функцию на более маленькие части, каждая из которых отвечала бы только за часть всего состояния приложения. Например отдельный reducer, который отвечает за события маршрутизации, за работу с пользователем и отдельные под разные модули. Эту задачу и решает функция combineReducer.
Теперь настало время взглянуть в исходный код:

Господа, проследуйте в репозиторий. Вот ссылочка на интересующий нас файл combineReducer.js.
Первым делом взглянем на экспортируемую функцию.
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)
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 errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
nextState[key] = nextStateForKey
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
return hasChanged ? nextState : state
}
}
Функция принимает объект, названия полей которого должны совпадать с узлами дерева state'а, а значение этих полей - функции, reducer'ы, которую обрабатывают эту часть state'а.
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]
}
}
По сути, всё что делает этот участок кода, это проверяет, что переданный объект соответствует формату, описанному выше. Каждое поле объекта должно быть функцией.
let shapeAssertionError
try {
assertReducerShape(finalReducers)
} catch (e) {
shapeAssertionError = e
}
Здесь вызывается функция, которая занимается проверкой того, что каждая reducer функция, соответствует предъявляемым к ней требованиям. А именно корректная реакция на событие INIT (возвращение default'ного состояния) и на несуществующее событие. В обоих случаях функция не может возвращать undefined.
unction 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(
`Reducer "${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.`
)
}
const type =
'@@redux/PROBE_UNKNOWN_ACTION_' +
Math.random()
.toString(36)
.substring(7)
.split('')
.join('.')
if (typeof reducer(undefined, { type }) === 'undefined') {
throw new Error(
`Reducer "${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.`
)
}
})
}
Последним шагом основной функции является возврат новой функции reducer'а, которая является комбинацией входных функций.
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 errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
nextState[key] = nextStateForKey
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
return hasChanged ? nextState : state
}
}
Давайте подробнее взглянем на основной цикл:
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 errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
nextState[key] = nextStateForKey
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
return hasChanged ? nextState : state
Здесь поочереди вызывается каждая функция и передаётся в неё только часть состояния (определяется по имени поля объекта пришедшего в combineReducer). Результат собирается назад в новый объект. Вот примерно так оно и работает. Если вы хотите сделать несколько уровней декомпозиции, то на каждом нужно вызывать combineReducer.
Напоследок пример использования данной функции:
import {combineReducers} from 'redux';
import page from './objects';
import {routerReducer} from 'react-router-redux';
import user from './user';
const rootReducer = combineReducers({
page,
user,
routing: routerReducer,
});
export default rootReducer;
На этом я, пожалуй, откланяюсь.