Урок 13: Кастомные middleware в Redux Toolkit

Приветствую всех на нашем курсе по Redux и Redux Toolkit! Мы подходим к одной из самых интересных тем, это созданию собственных промежуточных слоев или middleware.

Этот урок станет для нас ключевым в понимании того, как устроен внутренний механизм Redux и как мы можем его гибко настраивать под свои нужды. Мы не просто пользователи библиотеки, мы становимся ее архитекторами.

До этого момента наш поток данных в Redux был прямолинейным и предсказуемым: UI -> Dispatch(Action) -> Reducer -> Store -> UI. Это прекрасная, простая модель, которая обеспечивает предсказуемость. Но что, если нам нужно сделать что-то между моментом отправки действия (dispatch) и моментом, когда оно достигнет редьюсера? Например, логировать действия и состояние, обрабатывать асинхронные запросы (как это делает createAsyncThunk под капотом) или даже отменять или модифицировать действия на лету.

Именно здесь на сцену выходит концепция middleware. Middleware это механизм, который позволяет перехватывать, обрабатывать и передавать дальше действия, прежде чем они попадут в редьюсер. Представьте себе конвейерную ленту, действие это деталь, которая движется по ленте. Middleware это рабочие станции на этой ленте, которые могут осмотреть деталь, добавить к ней что-то, заменить ее или даже не пропустить дальше.

Давайте теперь определим нашу новую схему четко, структурно:

Action -> Middleware 1 -> Middleware 2 -> … -> Reducer

Когда вы вызываете dispatch(action), это действие не попадает напрямую в редьюсер. Вместо этого оно проходит через цепочку всех подключенных middleware. Каждое middleware получает возможность:

  1. Получить отправленное действие.

  2. Выполнить любой код (логирование, асинхронный вызов API, таймер).

  3. Решить, передать действие дальше по цепочке, вызвав next(action).

  4. Или даже отменить его, не вызывая next.

  5. В некоторых случаях, создать и отправить совершенно новые действия.

Понимание этой механики открывает безграничные возможности для кастомизации логики вашего приложения.

Пишем простейший логгер

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

Middleware в Redux это функция, которая возвращает функцию, которая возвращает функцию. Звучит сложно? Давайте разберем по косточкам. Это так называемая «функция высшего порядка».

Вот как выглядит шаблон:

javascript
const myMiddleware = (storeAPI) => (next) => (action) => {
  // Здесь живет наша логика
  // 1. Мы имеем доступ к `storeAPI`, который содержит `getState` и `dispatch`
  // 2. Мы имеем функцию `next`, чтобы передать действие следующему middleware или, наконец, редьюсеру.
  // 3. Мы имеем само `action`, которое было диспатчено.
}

Давайте наполним этот шаблон жизнью для нашего логгера:

javascript
// loggerMiddleware.js

const loggerMiddleware = (storeAPI) => (next) => (action) => {
  // Логируем тип действия и состояние ДО его обработки
  console.log('Диспатч действия:', action.type);
  console.log('Текущее состояние (до):', storeAPI.getState());

  // Передаем действие следующему middleware в цепочке (или редьюсеру, если это последнее).
  // Результат, который возвращает `next(action)`, это обновленное состояние после редьюсера.
  // Точнее, это тот результат, который вернет следующий в цепочке middleware. Последний вернет обновленный стор.
  const result = next(action);

  // Логируем состояние ПОСЛЕ обработки действия редьюсером
  console.log('Новое состояние (после):', storeAPI.getState());

  // Возвращаем результат, чтобы следующий в цепочке dispatch (если он есть) получил его.
  return result;
};

export default loggerMiddleware;

Давайте разберем каждую строчку.

  1. (storeAPI) => .... Первый уровень получает объект с двумя методами: getState и dispatchgetState. Это наша лазейка, чтобы посмотреть, что творится в хранилище до обновления.

  2. (next) => .... Второй уровень получает функцию next. Вызов next(action) это способ сказать: «Хорошо, я закончил со своим делом, передай это действие дальше по цепочке». Без этого вызова действие никогда не достигнет редьюсера и состояние не обновится!

  3. (action) => .... Третий внутренний уровень, это то место, куда «приземляется» каждое отправленное действие. Здесь мы и производим все наши манипуляции.

В нашем логгере мы сначала выводим действие и текущее состояние. Затем вызываем next(action), чтобы действие ушло в редьюсер и обновило состояние. После этого мы снова вызываем storeAPI.getState(), чтобы увидеть результат работы редьюсера и логируем его.

Интеграция middleware в store с помощью configureStore

Наше middleware готово, но пока оно безработное. Чтобы оно начало работать, его нужно «нанять», подключить к нашему хранилищу. Мы помним, что для создания стора в Redux Toolkit мы используем функцию configureStore. У этого функция есть опция middleware, которая как раз для этого и предназначена.

Давайте откроем наш файл, где создается store (часто это app/store.js) и добавим наш логгер.

javascript
// app/store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
// Импортируем наш middleware
import loggerMiddleware from './middleware/loggerMiddleware';

export default configureStore({
  reducer: {
    counter: counterReducer,
    // ... другие редьюсеры
  },
  // Добавляем наше middleware в массив.
  // Обратите внимание: Redux Toolkit уже по умолчанию добавляет некоторые middleware (например, для обработки thunk).
  // Мы добавляем свои ПОСЛЕ них.
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(loggerMiddleware),
});

Что здесь происходит? Опция middleware ожидает функцию, которая должна вернуть массив middleware. Мы вызываем getDefaultMiddleware(), чтобы получить массив middleware, которые уже встроены в RTK и критически важны для работы (например, redux-thunk). Затем мы используем метод .concat(), чтобы добавить наш loggerMiddleware в конец этого массива.

Порядок middleware в массиве имеет значение! Действие будет проходить через них именно в том порядке, в котором они перечислены. Наше middleware для логирования будет последним в цепочке перед редьюсером, что позволяет нам видеть состояние уже после работы всех других middleware (например thunk’ов).

Теперь, если вы откроете ваше приложение и нажмете на кнопку увеличения счетчика, в консоли браузера вы увидите примерно такой вывод:

text
Диспатч действия: counter/increment
Текущее состояние (до): {counter: {value: 0}}
Новое состояние (после): {counter: {value: 1}}

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

Усложняем логгер и пишем анализатор производительности

Давайте закрепим наши знания, создав еще два полезных middleware.

Задача 1: «Умный» логгер

Наш текущий логгер выводит все подряд, что может создать много шума в консоли. Давайте сделаем его «умным». Пусть он:

  1. Логирует действия только в режиме разработки ( process.env.NODE_ENV !== 'production').

  2. Группирует логи для каждого действия в сворачиваемую группу в консоли, используя console.group() и console.groupEnd().

  3. Логирует не только тип, но и полное действие, включая payload.

javascript
// smartLoggerMiddleware.js

const smartLoggerMiddleware = (storeAPI) => (next) => (action) => {
  // Логируем только в dev-режиме
  if (process.env.NODE_ENV === 'production') {
    return next(action);
  }

  // Создаем группу в консоли с названием = тип действия
  console.group(action.type);

  // Логируем информацию
  console.log('Действие:', action);
  console.log('Состояние (до):', storeAPI.getState());

  // Передаем действие и получаем результат
  const result = next(action);

  // Логируем состояние после
  console.log('Состояние (после):', storeAPI.getState());

  // Закрываем группу
  console.groupEnd();

  return result;
};

export default smartLoggerMiddleware;

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

Задача 2: Middleware для измерения производительности

Представьте, что у вас есть сложное действие, обработка которого занимает много времени и вы хотите это отследить. Давайте создадим middleware, которое будет замерять время выполнения.

javascript
// performanceMiddleware.js

const performanceMiddleware = (storeAPI) => (next) => (action) => {
  // Засекаем время до обработки
  const startTime = performance.now();

  // Передаем действие дальше
  const result = next(action);

  // Засекаем время после обработки
  const endTime = performance.now();

  // Вычисляем разницу
  const executionTime = endTime - startTime;

  // Логируем только если выполнение заняло, к примеру, больше 10мс
  if (executionTime > 10) {
    console.warn(`⚠️ Обработка действия "${action.type}" заняла ${executionTime.toFixed(2)}мс. Возможно, стоит оптимизировать редьюсер.`);
  }

  return result;
};

Этот middleware помогает находить «узкие места» в вашей логике обновления состояния. Если обработка какого-то действия начинает занимать слишком много времени, вы сразу увидите предупреждение в консоли.

Что еще можно делать с Middleware?

Возможности middleware практически безграничны. Вот еще несколько идей для вдохновения:

  1. Обработка асинхронности (как thunk). Вы можете проверять, является ли действие функцией и если да, выполнять ее, передавая ей dispatch и getState. Именно так работает популярный redux-thunk.

  2. Синхронизация с LocalStorage. Middleware может автоматически сохранять часть состояния в локальное хранилище браузера при каждом определенном действии.

  3. Обработка ошибок. Можно создать middleware, которое перехватывает все действия с типом, оканчивающимся на '/rejected' (как у createAsyncThunk) и централизованно показывать уведомления об ошибках пользователю.

  4. Модификация действий. Например, middleware может автоматически добавлять к каждому действию временную метку (timestamp).

  5. Отмена действий. Вы можете проанализировать действие и, в зависимости от каких-то условий в состоянии, решить не передавать его дальше, не вызывая next(action).

Итоги 13-го урока

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

Начните с простого логгера, затем попробуйте написать что-то свое. Каждое новое middleware будет глубже погружать вас в архитектуру Flux и Redux, делая вас сильнее как инженера.

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

Полный курс с уроками по Redux и Redux Toolkit для начинающих

Поделиться статьей:
Поддержать автора блога

Поддержка автора осуществляется с помощью специальной формы ниже, предоставленной сервисом «ЮMoney». Все платёжные операции выполняются на защищённой странице сервиса, что обеспечивает их корректность и полную безопасность.

Персональные рекомендации
Оставить комментарий