Перейти к основному содержимому
Неофициальный Бета-перевод

Эта страница переведена PageTurner AI (бета). Не одобрена официально проектом. Нашли ошибку? Сообщить о проблеме →

Повторное использование логики редюсеров

По мере роста приложения в логике редюсеров начинают проявляться общие паттерны. Вы можете обнаружить, что несколько частей логики выполняют одинаковые операции для разных типов данных, и захотите уменьшить дублирование, переиспользуя общую логику для каждого типа данных. Или вам может понадобиться обрабатывать несколько "экземпляров" определённого типа данных в хранилище. Однако глобальная структура Redux-хранилища имеет свои компромиссы: она упрощает отслеживание общего состояния приложения, но может затруднить "таргетирование" действий, которые должны обновлять конкретную часть состояния, особенно при использовании combineReducers.

Например, предположим, что мы хотим отслеживать несколько счётчиков в приложении с именами A, B и C. Мы определяем базовый редюсер counter и используем combineReducers для настройки состояния:

function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}

const rootReducer = combineReducers({
counterA: counter,
counterB: counter,
counterC: counter
})

К сожалению, в этом подходе есть проблема. Поскольку combineReducers вызывает каждый редюсер среза с одним и тем же действием, диспетчеризация {type : 'INCREMENT'} приведёт к увеличению значений всех трёх счётчиков, а не только одного. Нам нужен способ обернуть логику counter, чтобы гарантировать обновление только нужного счётчика.

Настройка поведения с помощью редюсеров высшего порядка

Как определено в Разделении логики редюсеров, редюсер высшего порядка — это функция, которая принимает редюсер в качестве аргумента и/или возвращает новый редюсер. Его также можно рассматривать как "фабрику редюсеров". Сам combineReducers является примером такого редюсера. Мы можем использовать этот паттерн для создания специализированных версий наших редюсеров, где каждая версия реагирует только на определённые действия.

Два наиболее распространённых способа специализации редюсера: генерация новых констант действий с заданным префиксом/суффиксом или добавление дополнительной информации в объект действия. Вот как это может выглядеть:

function createCounterWithNamedType(counterName = '') {
return function counter(state = 0, action) {
switch (action.type) {
case `INCREMENT_${counterName}`:
return state + 1
case `DECREMENT_${counterName}`:
return state - 1
default:
return state
}
}
}

function createCounterWithNameData(counterName = '') {
return function counter(state = 0, action) {
const { name } = action
if (name !== counterName) return state

switch (action.type) {
case `INCREMENT`:
return state + 1
case `DECREMENT`:
return state - 1
default:
return state
}
}
}

Теперь мы можем использовать любой из этих подходов для создания специализированных редюсеров счётчиков и диспетчеризации действий, влияющих только на нужную часть состояния:

const rootReducer = combineReducers({
counterA: createCounterWithNamedType('A'),
counterB: createCounterWithNamedType('B'),
counterC: createCounterWithNamedType('C')
})

store.dispatch({ type: 'INCREMENT_B' })
console.log(store.getState())
// {counterA : 0, counterB : 1, counterC : 0}

function incrementCounter(type = 'A') {
return {
type: `INCREMENT_${type}`
}
}
store.dispatch(incrementCounter('C'))
console.log(store.getState())
// {counterA : 0, counterB : 1, counterC : 1}

Мы также можем модифицировать подход, создав более универсальный редюсер высшего порядка, который принимает как сам редюсер, так и имя/идентификатор:

function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}

function createNamedWrapperReducer(reducerFunction, reducerName) {
return (state, action) => {
const { name } = action
const isInitializationCall = state === undefined
if (name !== reducerName && !isInitializationCall) return state

return reducerFunction(state, action)
}
}

const rootReducer = combineReducers({
counterA: createNamedWrapperReducer(counter, 'A'),
counterB: createNamedWrapperReducer(counter, 'B'),
counterC: createNamedWrapperReducer(counter, 'C')
})

Можно пойти ещё дальше и создать универсальный фильтрующий редюсер высшего порядка:

function createFilteredReducer(reducerFunction, reducerPredicate) {
return (state, action) => {
const isInitializationCall = state === undefined;
const shouldRunWrappedReducer = reducerPredicate(action) || isInitializationCall;
return shouldRunWrappedReducer ? reducerFunction(state, action) : state;
}
}

const rootReducer = combineReducers({
// check for suffixed strings
counterA : createFilteredReducer(counter, action => action.type.endsWith('_A')),
// check for extra data in the action
counterB : createFilteredReducer(counter, action => action.name === 'B'),
// respond to all 'INCREMENT' actions, but never 'DECREMENT'
counterC : createFilteredReducer(counter, action => action.type === 'INCREMENT')
};

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

Помимо генерации редюсеров, вы можете аналогичным образом создавать генераторы действий, а также генерировать их одновременно с помощью вспомогательных функций. См. библиотеки Генераторы действий/редюсеров и Редюсеры для готовых утилит.

Паттерн "Редюсер для коллекции / элемента"

Этот паттерн позволяет работать с несколькими состояниями, используя общий редюсер для обновления каждого состояния на основе дополнительного параметра в объекте действия.

function counterReducer(state, action) {
switch(action.type) {
case "INCREMENT" : return state + 1;
case "DECREMENT" : return state - 1;
}
}

function countersArrayReducer(state, action) {
switch(action.type) {
case "INCREMENT":
case "DECREMENT":
return state.map( (counter, index) => {
if(index !== action.index) return counter;
return counterReducer(counter, action);
});
default:
return state;
}
}

function countersMapReducer(state, action) {
switch(action.type) {
case "INCREMENT":
case "DECREMENT":
return {
...state,
state[action.name] : counterReducer(state[action.name], action)
};
default:
return state;
}
}