Эта страница переведена PageTurner AI (бета). Не одобрена официально проектом. Нашли ошибку? Сообщить о проблеме →
Написание пользовательского middleware
- Когда использовать пользовательское middleware
- Стандартные паттерны для middleware
- Как обеспечить совместимость вашего middleware с другими проектами Redux
Middleware в Redux в основном используется для:
-
создания побочных эффектов при обработке действий,
-
изменения или отмены действий, либо для
-
изменения входных данных, принимаемых dispatch.
Большинство случаев применения попадают в первую категорию: например, Redux-Saga, redux-observable и RTK listener middleware создают побочные эффекты в ответ на действия. Эти примеры также показывают, что это очень распространённая потребность: возможность реагировать на действие не только изменением состояния.
Изменение действий можно использовать, например, для обогащения действия информацией из состояния или внешних источников, либо для троттлинга, дебаунсинга или ограничения их выполнения.
Самый очевидный пример изменения входных данных для dispatch — это Redux Thunk, который преобразует функцию, возвращающую действие, в само действие путём её вызова.
Когда использовать пользовательское middleware
В большинстве случаев вам не понадобится пользовательское middleware. Наиболее вероятный сценарий использования middleware — побочные эффекты, и существует множество пакетов, которые удобно реализуют побочные эффекты для Redux и достаточно долго используются, чтобы избежать тонких проблем, возникающих при самостоятельной реализации. Хорошими отправными точками являются RTK Query для управления состоянием на стороне сервера и RTK listener middleware для других побочных эффектов.
Вам всё же может понадобиться пользовательское middleware в двух случаях:
-
Если у вас всего один очень простой побочный эффект, добавление целого дополнительного фреймворка может быть избыточным. Просто убедитесь, что переключитесь на существующее решение, когда ваше приложение вырастет, вместо развития собственной кастомной реализации.
-
Если вам нужно изменять или отменять действия.
Стандартные паттерны для middleware
Создание побочных эффектов для действий
Это самый распространённый тип middleware. Вот как это выглядит в rtk listener middleware:
const middleware: ListenerMiddleware<S, D, ExtraArgument> =
api => next => action => {
if (addListener.match(action)) {
return startListening(action.payload)
}
if (clearAllListeners.match(action)) {
clearListenerMiddleware()
return
}
if (removeListener.match(action)) {
return stopListening(action.payload)
}
// Need to get this state _before_ the reducer processes the action
let originalState: S | typeof INTERNAL_NIL_TOKEN = api.getState()
// `getOriginalState` can only be called synchronously.
// @see https://github.com/reduxjs/redux-toolkit/discussions/1648#discussioncomment-1932820
const getOriginalState = (): S => {
if (originalState === INTERNAL_NIL_TOKEN) {
throw new Error(
`${alm}: getOriginalState can only be called synchronously`
)
}
return originalState as S
}
let result: unknown
try {
// Actually forward the action to the reducer before we handle listeners
result = next(action)
if (listenerMap.size > 0) {
let currentState = api.getState()
// Work around ESBuild+TS transpilation issue
const listenerEntries = Array.from(listenerMap.values())
for (let entry of listenerEntries) {
let runListener = false
try {
runListener = entry.predicate(action, currentState, originalState)
} catch (predicateError) {
runListener = false
safelyNotifyError(onError, predicateError, {
raisedBy: 'predicate'
})
}
if (!runListener) {
continue
}
notifyListener(entry, action, api, getOriginalState)
}
}
} finally {
// Remove `originalState` store from this scope.
originalState = INTERNAL_NIL_TOKEN
}
return result
}
В первой части он обрабатывает действия addListener, clearAllListeners и removeListener, чтобы изменять список слушателей, которые должны быть вызваны позже.
Во второй части код в основном вычисляет состояние после прохождения действия через другие middleware и редюсеры, затем передаёт как исходное состояние, так и новое состояние (от редюсера) слушателям.
Побочные эффекты обычно выполняются после диспатчинга действия, поскольку это позволяет учитывать как исходное, так и новое состояние, и поскольку взаимодействия из побочных эффектов не должны влиять на текущее выполнение действия (иначе это не был бы побочный эффект).
Изменение/отмена действий или модификация ввода dispatch
Хотя эти паттерны менее распространены, большинство из них (кроме отмены действий) используются в redux thunk middleware:
const middleware: ThunkMiddleware<State, BasicAction, ExtraThunkArg> =
({ dispatch, getState }) =>
next =>
action => {
// The thunk middleware looks for any functions that were passed to `store.dispatch`.
// If this "action" is really a function, call it and return the result.
if (typeof action === 'function') {
// Inject the store's `dispatch` and `getState` methods, as well as any "extra arg"
return action(dispatch, getState, extraArgument)
}
// Otherwise, pass the action down the middleware chain as usual
return next(action)
}
Обычно dispatch может обрабатывать только JSON-действия. Это промежуточное ПО добавляет возможность обрабатывать действия в виде функций. Оно также изменяет тип возвращаемого значения самой функции dispatch, передавая возвращаемое значение функции-действия как результат вызова dispatch.
Правила создания совместимого промежуточного ПО
В принципе, промежуточное ПО — очень мощный шаблон, который может делать с действиями всё что угодно. Однако существующие решения могут предполагать определённое поведение соседних middleware, и понимание этих допущений упростит интеграцию вашего решения с популярными библиотеками.
Существует две точки взаимодействия между нашим middleware и другими решениями:
Вызов следующего промежуточного ПО
При вызове next следующее промежуточное ПО ожидает действие определённой формы. Если вы не планируете его модифицировать, просто передайте полученное действие без изменений.
Важный нюанс: некоторые middleware ожидают синхронного вызова в том же цикле событий, что и dispatch, поэтому ваш код должен вызывать next синхронно.
Возвращаемое значение dispatch
Если middleware не требует явного изменения возвращаемого значения dispatch, просто возвращайте результат вызова next. При необходимости модификации возвращаемого значения ваше ПО должно занимать строго определённое место в цепочке — потребуется ручная проверка совместимости со всеми другими middleware и согласование их совместной работы.
Это приводит к нетривиальному следствию:
const middleware: Middleware = api => next => async action => {
const response = next(action)
// Do something after the action hits the reducer
const afterState = api.getState()
if (action.type === 'some/action') {
const data = await fetchData()
api.dispatch(dataFetchedAction(data))
}
return response
}
Хотя кажется, что мы не меняем ответ, фактически это не так: из-за async/await он превращается в промис. Это сломает некоторые middleware, например из RTK Query.
Как же правильно реализовать такое middleware?
const middleware: Middleware = api => next => action => {
const response = next(action)
// Do something after the action hits the reducer
const afterState = api.getState()
if (action.type === 'some/action') {
void loadData(api)
}
return response
}
async function loadData(api) {
const data = await fetchData()
api.dispatch(dataFetchedAction(data))
}
Вынесите асинхронную логику в отдельную функцию — это позволит использовать async/await, но не ожидать разрешения промиса в самом middleware. Ключевое слово void явно указывает на сознательное решение не ожидать промис без влияния на выполнение кода.
Следующие шаги
Если вы ещё не знакомы с темой, изучите раздел о middleware в «Understanding Redux», чтобы понять внутренние механизмы работы промежуточного ПО.