При работе с редукторами в Redux некоторые шаблоны имеют тенденцию появляться и повторяться. Чтобы решить эту проблему, мы можем использовать редукторы высшего порядка и состав редукторов.

Редукторы высшего порядка - это функции, которые возвращают редукторы и / или принимают их в качестве аргументов. Редукторы для списков обычно необходимы во всех приложениях. Фабрика редукторов для создания редукторов списков может выглядеть так:

Где actionTypes - это такой объект, как:

actionTypes = {
  SUCCESS: 'FETCH_SUCCESS’,
  REQUEST: 'FETCH_REQUEST’,
  INVALIDATE: 'FETCH_INVALIDATE’,
  FAILURE: 'FETCH_FAILURE’,
}

Мы используем combineReducers для создания редуктора списка из редукторов, созданных другими фабриками редукторов.

Теперь мы можем создавать редукторы списков для определенных типов действий, вызывая createList(actionTypes). Их можно использовать, как показано в тесте ниже.

Создание усилителя byKey

Теперь предположим, что мы хотим отфильтровать связанный ресурс и сохранить по одному списку для каждого из этих ресурсов. Мы можем создать еще одну функцию более высокого порядка, чтобы позаботиться об этом. Давайте начнем с чего-то более общего и создадим более конкретные реализации позже.

Теперь мы можем использовать createByKey для разделения редуктора по ключу. Он принимает два аргумента, кроме редуктора:

  1. Предикат, который выполняет действие и сообщает, следует ли применить редуктор.
  2. Функция, которая отображает действие на клавишу.

Обратите внимание, что мы разделили аргументы, так что возвращается функция с редуктором в качестве единственного аргумента. Это называется каррированием, и это означает, что мы будем вызывать его с помощью createByKey(pred, mapToKey)(reducer). Мы еще вернемся к тому, почему это полезно.

Мы можем протестировать это с помощью простого редуктора с начальным состоянием null, который заботится только о типе действия SUCCESS. Он и возвращает полезную нагрузку как новый state, если тип действия совпадает.

Комбинирование редукторов с фильтром

Также было бы неплохо, если бы мы могли комбинировать редукторы с фильтрацией. Давайте создадим редуктор более высокого порядка, который работает аналогично combineReducer, но вызывает редуктор только тогда, когда action.filterName соответствует ключу в объекте с редукторами.

Фактически, это очень похоже на то, что предоставляется createByKey. Основные отличия:

  • Он принимает объект в качестве аргумента, с именами фильтров в качестве ключей и редукторами в качестве значений.
  • Ему необходимо вычислить и объединить начальные состояния всех редукторов.
  • Для каждого ключа нужно использовать разные редукторы.
  • Он знает, как получить filterName из действия и как определить предикат.

Максимальная компоновка

Помните, что createByKey возвращает функцию с редуктором в качестве единственного аргумента. Мы можем создать две функции высшего порядка, которые возвращают функции с одним аргументом, скажем withInitialState(initialState) и mapToReducer(mapActionToKey). Если эти функции принимают выходные данные друг друга в качестве входных данных, мы можем использовать compose для их объединения. Если вы не знакомы с compose, он работает следующим образом:

compose(f, g, h)(x, y, z)
// is equivalent to
f(g(h(x, y, z)))

Давайте напишем эти функции и вспомогательную функцию combineInitialState, которая создает начальное состояние, комбинируя начальные состояния filterReducers.

Здесь нужно остерегаться mapToReducer. Он сработает, если mapActionToKey попытается получить доступ к несуществующему вложенному свойству. Однако мы используем предикат, переданный в createByKey, чтобы этого не произошло.

Собирая его вместе, мы можем объединить предыдущие функции с compose. Я использовал has из lodash в предикате, но его можно заменить на hasOwnProperty.

Отфильтрованную комбинацию редукторов теперь можно создать, вызвав createFilter({ [SOME_FILTER]: reducer, ... }). Ключи будут сопоставлены с action.filterName. В приведенном ниже тесте показано, как мы можем создать два фильтра для простого редуктора с помощью функции createFilter.

Мы можем использовать createList вместе с createFilter, поместив редуктор, возвращенный из createList в качестве редукторов фильтра, в createFilter.

Создание с помощью createFilterReducers

Теперь, если все, что нам нужно, - это создать фильтры для одного и того же редуктора, мы можем создать другую функцию, которая позаботится об этом. Аргументы этой функции должны быть массивом имен фильтров и редуктором.

Теперь мы можем использовать его с createFilter и createList. Поскольку у нас есть набор функций с одним аргументом, которые принимают друг друга в качестве входных данных, мы снова можем использовать compose.

Для некоторых фильтров мы можем захотеть сделать что-то еще, прежде чем передавать его редуктору фильтра. Давайте напишем функцию, которая принимает в качестве аргументов объект с редукторами и объект с редукторами более высокого порядка и возвращает новый объект с расширенными редукторами.

Мы можем использовать его, добавив в качестве аргумента между createFilter и createFilterReducers. Редуктор высшего порядка enhancer просто создает вложенное состояние с состоянием другого редуктора на enhanced ключе.

Предварительная настройка с помощью createEnhancedFilter

Эта композиция может оказаться обычным вариантом использования, поэтому мы можем создать другую функцию, которая принимает reducerNames и filterEnhancers и выполняет композицию.

Мы можем протестировать это с теми же reducer и enhancer, что и раньше.

Собираем все вместе с createByFilterQuery

Итак, у нас уже есть createByKey в качестве усилителя, который мы можем использовать для связанных ресурсов. Однако для этого требуются predicate и mapActionToKey в качестве аргументов, и они часто будут выглядеть очень похожими. Если id связанного ресурса представлен как { filterQuery: { [filterQueryKey]: id } }, мы можем создать функцию для установкиcreateByKey таким образом.

Мы также добавим в createFilter.js фабрику селекторов. В качестве аргумента он принимает объект с селекторами. Если filterArgs.filterName соответствует ключу в селекторах, он вызовет этот селектор с filterState перед передачей результата обратно. Это может помочь нам справиться с вложенными состояниями от энхансеров.

Чтобы собрать все вместе, мы составим 53_ с createList и будем использовать filterQuery усилитель для одного из фильтров.

Этот шаблон можно использовать для объединения других типов редукторов, таких как undo и pagination. Я закончу примером того, как это могло бы выглядеть. У нас есть три фильтра, все они будут относиться к спискам с разбивкой на страницы. У двух редукторов фильтров есть усилители для обработки связанных ресурсов. Для FILTER_CREATED_YEAR мы вложили вложенный усилитель просто для этого.

Я считаю, что это позволяет повторно использовать хороший код при создании других редукторов и редукторов более высокого порядка, а не переделывать его. Они с нуля состоят из довольно простых редукторов с единичными задачами. Большинство редукторов более высокого порядка придерживаются подхода, когда они хотят знать как можно меньше о вещах, которые им не нужны.

Как мне это сделать? Не знаю, правда не хочу знать. Вы поймете это!

- Редуктор высшего порядка

Я не знаю, является ли использование compose повсюду безумием или нет. Возможно, придется немного привыкнуть, но это определенно хорошо :)