Зарегистрируйтесь, чтобы продолжить обучение

Мидлвары React: Redux Toolkit

Мидлвары (middlewares) — это функции, которые последовательно вызываются во время обновления данных в хранилище. Они относятся к продвинутым техникам использования Redux. В этом уроке мы разберем, как подключать и использовать мидлвары.

Для чего нужны мидлвары

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

Представим, что мы хотим добавить какую-то логику при выполнении любого действия в redux. Самый простой пример — это логирование. Для этого воспользуемся тремя методами:

  • console.info()
  • console.groupEnd()
  • console.log()

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

const logger = (store, action) => {
  console.group(action.type);
  console.info('Action: ', action);
  store.dispatch(action);
  console.log('Next state', store.getState()); // Выводим новое состояние в консоль
  console.groupEnd();
};

// Используем вместо обычного store.dispatch()
logger(store, increment({ type: 'INCREMENT' }));

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

Поэтому для таких задач используются мидлвары — это отдельные изолированные функции, которые встраиваются в работу и вызываются по цепочке. Весь процесс похож на конвейер, по которому движется состояние: каждый обработчик-мидлвара выполняет какую-то свою работу и изменяет состояние при необходимости.

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

Как работают мидлвары

Общий принцип такой:

  • Сначала мидлвары встраиваются в хранилище при его создании
  • Затем начинается отправка действий (диспатчинга)
  • В этот момент данные проходят через мидлвары и затем попадают в редьюсер

Схематично этот принцип можно показать так:

Работа мидлваров в Redux

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

Мидлвары используются в таких задачах:

  • Логирование
  • Оповещение об ошибках
  • Работа с асинхронным API
  • Маршрутизация

Как устроены мидлвары

Рассмотрим типичную структуру мидлвары. Любая мидлвара в Redux состоит из нескольких вложенных функций:

const logger = (store) => {
  return (next) => {
    return (action) => {
      // Код мидлвары
    };
  };
};

Разберем каждую функцию:

  1. Внешняя функция

    Эта функция нужна для подключения мидлвары к хранилищу. Она передается в Redux-метод applyMiddleware(). Функция получает на вход объект store, который содержит методы dispatch() и getState() для работы с Redux внутри мидлвары.

  2. Первая вложенная функция

    Ее аргументом будет функция next(). Вызов этой функции в теле мидлвары с действием в качестве аргумента может прокидывать действие дальше по цепочке мидлвар. Если функция next() вызвана в последней мидлваре цепочки, то она передает действие в редьюсер и вызывает обновление состояния. Чуть позже мы более подробно это разберем.

  3. Вторая вложенная функция

    В качестве аргумента эта функция принимает действие action при его отправке. Мидлвара перехватит любое действие в приложении, отправленное в редьюсер.

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

const logger = (store) => (next) => (action) => {
  // Код мидлвары
};

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

  • store
  • next
  • action

Подключение мидлвары

Для подключения мидлвары, достаточно вызвать applyMiddleware(), передав в него мидлвару, а результат передать вторым параметром в createStore():

import { createStore, applyMiddleware } from 'redux';

const logger = (store) => (next) => (action) => {
  // Код мидлвары
};

const store = createStore(
  reducer,
  applyMiddleware(logger),
);

Если нужно добавить несколько мидлвар, то нужно использовать дополнительный метод compose():

import { createStore, applyMiddleware, compose } from 'redux';

const logger = (store) => (next) => (action) => {
  // Код мидлвары
};

const updater = (store) => (next) => (action) => {
  // Код мидлвары
};

const store = createStore(
  reducer,
  compose(
    applyMiddleware(logger),
    applyMiddleware(updater),
  ),
);

При диспатче экшена мидлвары будут выполняться в том порядке, в котором были добавлены. Например, выше в примере, мидлвары будут выполняться в следующем порядке logger -> updater -> reducer.

Как использовать мидлвары

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

  • console.info()
  • console.groupEnd()
  • console.log()

Рассмотрим сам код:

// Мидлвара со вложенными стрелочными функциями
const logger = (store) => (next) => (action) => {
  console.group(action.type);
  console.info('dispatching', action);
  const result = next(action);
  console.log('next state', store.getState()); // Выводим новое состояние в консоль
  console.groupEnd();
  return result;
};

Подключение мидлвары:

import { createStore, applyMiddleware, compose } from 'redux';

import { logger } from './middlewares.js';

const store = createStore(
  reducer,
  applyMiddleware(logger),
);

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

store.dispatch({ type: 'INCREMENT' });
/*
INCREMENT
dispatching Object { type: "INCREMENT", payload: {…} }
next state 1
*/

Мидлвары с асинхронными вызовами

Мидлвара может содержать асинхронную логику, например:

const fetchNewData = (store) => (next) => (action) => {
  if (action.type === 'UPDATE') {
    axios.get('/data').then((data) => {
      store.dispatch({ type: 'dataLoaded', payload: data });
    });
  }

  return next(action);
};

В примере выше мы добавили мидлвару, которая загружает новые данные, если передано действие UPDATE. Мидлвара не блокирует выполнение текущего действия и вызывает dispatch(), когда данные будут загружены. Такой диспатч запустит новую цепочку мидлвар и выполнение действия. С диспатчем внутри мидлвар нужно быть аккуратнее, потому что диспатч запускает новую цепочку, из-за чего могут возникнуть рекурсивные вызовы мидлвары.

Функция next()

Вы уже могли заметить вызовы функции next() в примерах выше. Как и следует из ее названия, эта функция вызывает следующую в цепочке мидлвару. Если текущая мидлвара уже последняя, то вызывается редьюсер.

Тут может возникнуть вопрос: «Как может вызываться следующая в цепочке мидлвара, если текущая еще не завершила работу?». Технически, это возможно. Мидлвары мы подключаем заранее, поэтому на момент срабатывания экшена в Redux уже хранятся все мидлвары. Redux может запустить одну мидлвару внутри другой, ведь это обычные функции.

На самом деле именно вызов next() и обеспечивает цепочку вызовов мидлвар. Если хотя бы в одной мидлваре не будет вызова next(), то вся цепочка на этой мидлваре оборвется, и конечный редьюс не вызовется.

Разберем работу next() на работе нескольких мидлвар в цепочке:

const getCurrentUser = () => ({ name: 'Ivan' });

const logger = (store) => (next) => (action) => {
  console.group(action.type);
  console.info('dispatching', action);
  const result = next(action);
  console.log('next state', store.getState());
  console.groupEnd();
  return result;
};

const addFinishText = (store) => (next) => (action) => {
  if (action.type !== 'TASK_FINISH') {
    return next(action);
  }

  const user = getCurrentUser();
  action.payload.task.text = [action.payload.task.text, `Задачу завершил ${user.name}`].join('. ');
  return next(action);
};

const store = createStore(
  reducer,
  compose(
    applyMiddleware(logger),
    applyMiddleware(addFinishText),
  ),
);

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

Мидлвара addFinishText() добавляет в payload имя текущего пользователя. Благодаря этому в редьюсер попадет уже измененный текст с именем пользователя.

Во всех примерах мы возвращаем результат из мидлвары:

return next(action);

Если просто вызывать next() без возврата результата, то в нашем примере на самом деле ничего не сломается. Логирование по-прежнему будет выводить все данные и будет добавляться имя пользователя. Но такой возврат нужен, чтобы мидлвара могла видеть результат следующей мидлвары. Например, это позволяет строить цепочки мидлвар, использующие промисы, когда одна мидлвара должна дождаться завершения промиса другой мидлвары. Хорошей практикой считается всегда возвращать результат next() из мидлвары.

Выводы

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


Дополнительные материалы

  1. Официальная документация Middleware

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
от 25 000 ₸ в месяц
Разработка фронтенд-компонентов для веб-приложений
10 месяцев
с нуля
Старт 26 декабря
профессия
от 39 525 ₸ в месяц
Разработка фронтенд- и бэкенд-компонентов для веб-приложений
16 месяцев
с нуля
Старт 26 декабря

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»