null

Понимаем как работает функция combineReducer из Redux

Добро пожаловать в чудный мир эльфов и волшебства 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;

 

На этом я, пожалуй, откланяюсь.