При работе с редукторами в Redux некоторые шаблоны имеют тенденцию появляться и повторяться. Чтобы решить эту проблему, мы можем использовать редукторы высшего порядка и состав редукторов.
Редукторы высшего порядка - это функции, которые возвращают редукторы и / или принимают их в качестве аргументов. Редукторы для списков обычно необходимы во всех приложениях. Фабрика редукторов для создания редукторов списков может выглядеть так:
Где actionTypes
- это такой объект, как:
actionTypes = { SUCCESS: 'FETCH_SUCCESS’, REQUEST: 'FETCH_REQUEST’, INVALIDATE: 'FETCH_INVALIDATE’, FAILURE: 'FETCH_FAILURE’, }
Мы используем combineReducers
для создания редуктора списка из редукторов, созданных другими фабриками редукторов.
Теперь мы можем создавать редукторы списков для определенных типов действий, вызывая createList(actionTypes)
. Их можно использовать, как показано в тесте ниже.
Создание усилителя byKey
Теперь предположим, что мы хотим отфильтровать связанный ресурс и сохранить по одному списку для каждого из этих ресурсов. Мы можем создать еще одну функцию более высокого порядка, чтобы позаботиться об этом. Давайте начнем с чего-то более общего и создадим более конкретные реализации позже.
Теперь мы можем использовать createByKey
для разделения редуктора по ключу. Он принимает два аргумента, кроме редуктора:
- Предикат, который выполняет действие и сообщает, следует ли применить редуктор.
- Функция, которая отображает действие на клавишу.
Обратите внимание, что мы разделили аргументы, так что возвращается функция с редуктором в качестве единственного аргумента. Это называется каррированием, и это означает, что мы будем вызывать его с помощью 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
повсюду безумием или нет. Возможно, придется немного привыкнуть, но это определенно хорошо :)