Мидлвары — 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' >));
Это рабочий код, но в нем есть значительные недостатки. Представим, что мы хотим добавить еще какую-то логику — например, дополнительное логирование или запросы на сервер. В таком случае нам пришлось бы расширять эту функцию. Такой подход не очень удобен, потому что со временем код функции становится слишком большим, и вся логика смешивается.
Поэтому для таких задач используются мидлвары — это отдельные изолированные функции, которые встраиваются в работу и вызываются по цепочке. Весь процесс похож на конвейер, по которому движется состояние: каждый обработчик-мидлвара выполняет какую-то свою работу и изменяет состояние при необходимости.
При этом в работе мидлвар есть некоторые особенности, которые отличают работу мидлвар от классического конвейера. О них мы поговорим позже.
Как работают мидлвары
Общий принцип такой:
- Сначала мидлвары встраиваются в хранилище при его создании
- Затем начинается отправка действий (диспатчинга)
- В этот момент данные проходят через мидлвары и затем попадают в редьюсер
Схематично этот принцип можно показать так:
Благодаря такой организации программисты могут расширять библиотеки новой функциональностью, не переписывая исходный код под конкретную задачу.
Мидлвары используются в таких задачах:
- Логирование
- Оповещение об ошибках
- Работа с асинхронным API
- Маршрутизация
Как устроены мидлвары
Рассмотрим типичную структуру мидлвары. Любая мидлвара в Redux состоит из трех вложенных функций:
const logger = (store) => return (next) => return (action) => // Код мидлвары >; >; >;
Разберем каждую функцию:
- Внешняя функция Эта функция нужна для подключения мидлвары к хранилищу. Она передается в Redux-метод applyMiddleware() . Функция получает на вход объект store , который содержит методы dispatch() и getState() для работы с Redux внутри мидлвары.
- Первая вложенная функция Ее аргументом будет функция next() . Вызов этой функции в теле мидлвары с действием в качестве аргумента может прокидывать действие дальше по цепочке мидлвар. Если функция next() вызвана в последней мидлваре цепочки, то она передает действие в редьюсер и вызывает обновление состояния. Чуть позже мы более подробно это разберем.
- Вторая вложенная функция В качестве аргумента эта функция принимает действие action при его отправке. Мидлвара перехватит любое действие в приложении, отправленное в редьюсер.
Такая вложенность функций может смутить, но так устроены мидлвары и это остается только принять. Рекомендуется использовать короткую форму объявления функций:
const logger = (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() из мидлвары.
Открыть доступ
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно
- 130 курсов, 2000+ часов теории
- 1000 практических заданий в браузере
- 360 000 студентов
Наши выпускники работают в компаниях:
Как делать асинхронные Redux экшены используя Redux-Thunk
По умолчанию, экшены в Redux являются синхронными, что, является проблемой для приложения, которому нужно взаимодействовать с серверным API, или выполнять другие асинхронные действия. К счастью Redux предоставляет нам такую штуку как middleware, которая стоит между диспатчом экшена и редюсером. Существует две самые популярные middleware библиотеки для асинхронных экшенов в Redux, это — Redux Thunk и Redux Saga. В этом посте мы будем рассматривать первую.
Redux Thunk это middleware библиотека, которая позволяет вам вызвать action creator, возвращая при этом функцию вместо объекта. Функция принимает метод dispatch как аргумент, чтобы после того, как асинхронная операция завершится, использовать его для диспатчинга обычного синхронного экшена, внутри тела функции.
Если вам интересно, то Thunk, это концепт в мире программирования, когда функция используется для задержки выполнения операции.
Установка и настройка
Во первых, добавьте redux-thunk пакет в ваш проект:
$ yarn add redux-thunk # или, с помощью npm: $ npm install redux-thunk
Затем, добавьте middleware, когда будете создавать store вашего приложения, с помощью applyMiddleware, предоставляемый Redux’ом:
import React from 'react'; import ReactDOM from 'react-dom'; import < createStore, applyMiddleware >from 'redux'; import < Provider >from 'react-redux'; import thunk from 'redux-thunk'; import rootReducer from './reducers'; import App from './App'; // используй applyMiddleware, чтобы добавить thunk middleware к стору const store = createStore(rootReducer, applyMiddleware(thunk)); ReactDOM.render( > , document.getElementById('root') );
Основное использование
Обычно Redux-Thunk используют для асинхронных запросов к внешней API, для получения или сохранения данных. Redux-Thunk позволяет легко диспатчить экшены которые следуют «жизненному циклу» запроса к внешней API.
Например, у нас есть обычное todo приложение. Когда мы нажимаем «добавить todo», обычно, сперва диспатчится экшен, который сообщает о старте добавления нового todo. Затем, если todo элемент успешно создан и возвращен сервером, диспатчится другой экшен, с нашим новым todo элементом, и операция завершается успешно. В случае, если сервер по каким то причинам возвращает ошибку, то вместо добавления нового todo диспатчится экшен с ошибкой, что операция не была завершена.
Давайте посмотрим, как это может быть реализовано с помощью Redux-Thunk. В компоненте, экшен диспатчится как обычно:
import < connect >from 'react-redux'; import < addTodo >from '../actions'; import NewTodo from '../components/NewTodo'; const mapDispatchToProps = dispatch => < return < onAddTodo: todo => < dispatch(addTodo(toto)); >>; >; export default connect( null, mapDispatchToProps )(NewTodo);
В самом экшене дело обстоит намного интереснее. Здесь мы будем использовать библиотеку Axios, для ajax запросов. Если она у вас не установлена, то добавьте ее так:
# Yarn $ yarn add axios # npm $ npm install axios --save
Мы будем делать POST запрос на адрес — jsonplaceholder.typicode.com/todos:
actions/index.js
import < ADD_TODO_SUCCESS, ADD_TODO_FAILURE, ADD_TODO_STARTED, DELETE_TODO >from './types'; import axios from 'axios'; export const addTodo = (< title, userId >) => < return dispatch =>< dispatch(addTodoStarted()); axios .post(`https://jsonplaceholder.typicode.com/todos`, < title, userId, completed: false >) .then(res => < dispatch(addTodoSuccess(res.data)); >) .catch(err => < dispatch(addTodoFailure(err.message)); >); >; >; const addTodoSuccess = todo => ( < type: ADD_TODO_SUCCESS, payload: < . todo >>); const addTodoStarted = () => (< type: ADD_TODO_STARTED >); const addTodoFailure = error => ( < type: ADD_TODO_FAILURE, payload: < error >>);
Обратите внимание, как наш addTodo action creator возвращает функцию, вместо обычного экшен объекта. Эта функция принимает аргумент dispatch из store.
Внутри тела функции мы сперва диспатчим обычный синхронный экшен, который сообщает, что мы начали добавление нового todo с помощью внешней API. Простыми словами — запрос был отправлен на сервер. Затем, мы собственно делаем POST запрос на сервер использую Axios. В случае утвердительного ответа от сервера, мы диспатчим синхронный экшен, используя данные, полученные из сервера. Но в случае ошибки от сервера мы диспатчим другой синхронный экшен с сообщением ошибки.
Когда мы используем API, который действительно является внешним (удаленным), как JSONPlaceholder в нашем случае, легко заметить что происходит задержка, пока ответ от сервера не приходит. Но если вы работаете с локальным сервером, ответ может приходить слишком быстро, так что вы не заметите задержки. Так-что для своего удобства, вы можете добавить искусственную задержку при разработке:
actions/index.js (кусок кода)
export const addTodo = (< title, userId >) => < return dispatch =>< dispatch(addTodoStarted()); axios .post(ENDPOINT, < title, userId, completed: false >) .then(res => < setTimeout(() =>< dispatch(addTodoSuccess(res.data)); >, 2500); >) .catch(err => < dispatch(addTodoFailure(err.message)); >); >; >;
А для тестирования сценария с ошибкой, вы можете напрямую выбросить ошибку:
actions/index.js (кусок кода)
export const addTodo = (< title, userId >) => < return dispatch =>< dispatch(addTodoStarted()); axios .post(ENDPOINT, < title, userId, completed: false >) .then(res => < throw new Error('NOT!'); // dispatch(addTodoSuccess(res.data)); >) .catch(err => < dispatch(addTodoFailure(err.message)); >); >; >;
Для полноты картины, вот пример, как наш todo редюсер может выглядеть, что-бы обрабатывать полный «жизненный цикл» запроса:
reducers/todoReducer.js
import < ADD_TODO_SUCCESS, ADD_TODO_FAILURE, ADD_TODO_STARTED, DELETE_TODO >from '../actions/types'; const initialState = < loading: false, todos: [], error: null >; export default function todosReducer(state = initialState, action) < switch (action.type) < case ADD_TODO_STARTED: return < . state, loading: true >; case ADD_TODO_SUCCESS: return < . state, loading: false, error: null, todos: [. state.todos, action.payload] >; case ADD_TODO_FAILURE: return < . state, loading: false, error: action.payload.error >; default: return state; > >
getState
Функция, возвращаемая асинхронным action creator’ом с помощью Redux-Thunk, также принимает getState метод как второй аргумент, что позволяет получать стейт прямо внутри action creator’а:
actions/index.js (кусок кода)
export const addTodo = (< title, userId >) => < return (dispatch, getState) =>< dispatch(addTodoStarted()); console.log('current state:', getState()); // . >; >;
При выполнении этого кода, текущий стейт просто будет выведен в консоль. Например:
Использование getState может быть действительно полезным, когда надо реагировать по разному, в зависимости от текущего стейта. Например, если мы ограничили максимальное количество todo элементов до 4, мы можем просто выйти из функции, если этот лимит превышается:
actions/index.js (кусок кода)
export const addTodo = (< title, userId >) => < return (dispatch, getState) => < const < todos >= getState(); if (todos.length >= 4) return; dispatch(addTodoStarted()); // . >; >;
Забавный факт — а вы знали что код Redux-Thunk состоит только из 14 строк? Можете проверить сами, как Redux-Thunk middleware работает под капотом
Совершенствуем Redux
Привет, Хабр! Представляю вашему вниманию перевод статьи «Redesigning Redux» автора Shawn McKay.
Должно ли управление состоянием вызывать проблемы на сегодняшний день? Интуитивно, разработчики видят скрытую правду: управление состоянием куда сложнее, чем должно быть. В данной статье мы разберем несколько вопросов, которые вы наверняка задавали себе:
- Вам действительно необходима библиотека для управления состоянием?
- Заслужил ли Redux свою популярность? Почему или почему нет?
- Можем ли мы придумать лучшее решение? Если да, то какое?
Необходима ли библиотека для управления состоянием?
Front-end разработчик не тот, кто попросту передвигает пиксели из стороны в сторону; истинное искусство в знании где хранить состояние. Это кажется сложным только на первый взгляд.
Давайте рассмотрим возможности, которые предоставляет нам React:
1. Состояние компонента (Component State)
Состояние хранится внутри компонента. В React мы обновляем state через setState() .
2. Относительное состояние (Relative State)
Состояние переданное от родителя потомку. В React передаем props как свойство компонента потомка.
3. Переданное состояние (Provided State)
Состояние хранится в поставщике (provider), и доступно любому компоненту (consumer), расположенному ниже по дереву. Context API в React.
View хранит большую часть состояния. Но что делать с остальным кодом, который отражает основные данные и логику?
Размещение всего кода внутри компонентов может привести к низкому разделению ответственности: растет зависимость от view-библиотек, усложняется тестирование такого кода, но самое страшное: приходится регулярно менять способ хранения состояния.
Структура приложения со временем изменяется, и с каждым разом сложнее определить, какому компоненту необходимо то или иное состояние. Проще всего вынести все состояние из корневого компонента, что можно осуществить следующим способом.
4. Внешнее состояние (External State)
Состояние может находиться отдельно от компонентов, которые синхронно «свяжуться» с ним при помощи provider/consumer паттерна. Вероятнее всего самой большой популярностью, среди библиотек для управления состоянием, пользуется Redux. В течении последних двух лет она получила большую известность среди разработчиков. Так в чем причина такой любви к одной библиотеке?
Redux более производительный? Нет. На самом деле, работа приложения немного замедляется с каждым новым действием, которое должно быть обработано.
Redux прост в применении? Конечно нет.
Простым был бы нативный javascript:

Так почему каждый не может использовать global.state = <> ?
Почему Redux?
Под капотом, Redux аналогичен глобальному объекту TJ, только обернут рядом утилит.

В Redux можно непосредственно изменять состояние, путем передачи (dispatch) действий (action) через указанные инструменты.
Библиотека включает два вида обработчиков действий: middleware & subscriptions. Middleware — это функции, которые перехватывают действия. Включают такие инструменты как «logger», «devtools» или «syncWithServer». Subscriptions — это функции, используемые для отправки изменений компонентам.
Наконец, редьюсеры (reducer) — это функции, которые изменяют состояние и делят его на мелкие, модульные и управляемые части.
Вероятнее всего, Redux более применим для хранения состояния, чем глобальный объект.
Думайте о Redux как о глобальном объекте с расширенными возможностями и упрощенным способом «преобразования» состояния.
Настолько ли сложен Redux?
Да. Есть несколько неоспоримых признаков, что необходимо улучшить API; можно сделать вывод при помощи следующего уравнения:

Считаем, что time_saved подразумевает время затраченное на разработку собственного решения, а time_invested равняется часам потраченным на чтение документации, прохождение обучающих курсов и изучение новых понятий.
Redux, в принципе, простая и небольшая библиотека с крутой кривой обучения. На каждого разработчика, который овладел и извлек выгоду из Redux, погрузившись в функциональное программирование, найдется другой потенциальный разработчик, запутавшийся и думающий «это все не для меня, я возвращаюсь к jQuery».
Вы не должны разбираться что такое «comonad», используя jQuery, и не обязаны понимать функциональную композицию, чтобы справиться с управлением состоянием.
Цель любой библиотеки: сделать сложное простым при помощи абстракции.
Я не намерен высмеивать Дэна Абрамова. Redux стал популярным на слишком ранней стадии своего развития.
- Как внести изменения в библиотеку, которую используют миллионы разработчиков?
- Как вы оправдаете критические изменения, которые повлияют на проекты во всем мире?
А может есть другой путь?
Совершенствуем Redux
Redux заслуживает изменений, и я вооружился шестью его слабыми местами, чтобы доказать это.
1. Настройка
Предлагаю посмотреть на первоначальную настройку Redux-приложения (левый скрин).

Много разработчиков, сразу после первого шага, остановились в недоумении. Что такое thunk? compose? Способна ли функция на такое?
Считается, что Redux основан на конфигурации над композицией. Настройка должна быть похожа на пример справа.
2. Упрощаем редьюсеры
Редьюсеры в Redux могут использовать switch-конструкции далекие от тех, которые мы привыкли использовать.

Учитывая, что редьюсеры находят соответствие по типу действия, мы можем сделать каждый редьюсер чистой (pure) функцией, принимающей состояние и действие. Можно сократить действие и передавать только состояние и данные.
3. Async/Await без Thunk
Thunk широко используется для создания асинхронных действий в Redux. Во многих отношениях, thunk больше похож на умный хак, чем на официально рекомендуемое решение. Как это работает:
- Вы передаете действие, уже как функцию, а не объект.
- Thunk проверяет каждое действие, что оно является функцией.
- Если все сходится, thunk вызывает эту функцию и передает в нее некоторые методы стора: dispatch и getState.

Возможно ли использовать async/await, как в примере справа?
4. Два вида действий
Задумайтесь, ведь действительно есть два вида действий:
- Reducer action: запускает редьюсер и изменяет состояние.
- Effect action: запускает асинхронное действие. Может вызвать reducer action, но асинхронная функция не способна напрямую изменить состояние.
5. Никаких больше переменных хранящих тип действия
Почему принято разделять генераторы действий (action creators) и редьюсеры? Может ли один существовать без другого? Как изменить один не изменяя другой?
Генераторы действий и редьюсеры две стороны одной медали.
const ACTION_ONE = ‘ACTIONE_ONE’ — это лишний побочный эффект разделения генераторов действий и редьюсеров. Обращайтесь с ними как с единым целом и отпадет потребность в крупных файлах с экспортом типов.
6. Редьюсеры — это генераторы действий
Объединяйте элементы Redux по их назначению, и вы получите простой шаблон.

В итоге, следуя этому сценарию, редьюсер может стать генератором действий.
Используйте соглашение об именах, и следующие пункты будут довольно предсказуемы:
- Если редьюсер получил название «increment», тогда тип будет «increment». Даже лучше, обозначим как «count/increment».
- Каждое действие передает данные через «payload».

Теперь при помощи count.increment мы можем создать генератор действий напрямую из редьюсера.
Хорошие новости: мы можем улучшить Redux
Эти проблемные части мотивировали на создание Rematch.

Rematch служит оберткой вокруг Redux, предоставляя разработчикам упрощенное API.

Вот полный пример кода с Rematch:

Я использовал Rematch в production последние несколько месяцев. И что я думаю:
Я никогда не тратил так мало времени на управление состоянием.
Redux не исчезнет и не обязан. Освойте эту библиотеку с меньшей кривой обучения, меньшим количеством бойлерплейта и меньшими умственными затратами.
Опробуйте Rematch и выясните, нравится он вам или нет.
Поставьте звезду, чтобы позволить узнать о нас другим.
Зачем нам нужны middleware redux?
Чем такой способ dispatch’а отличается от подобного, с использованием redux-thunk?
export const loginAction = (data) => async (dispatch) => < try < const res = await api.login(data); if(res.status < 300) < dispatch(setUserData(res.data)) >> catch(err) < alert(err) >>
Есть ли какая-нибудь разница вообще? И если есть, то какой способ лучше использовать и почему? Почему redux настоятельно рекомендует использовать инструменты по типу middleware redux-thunk, ведь мои действия и так асинхронны, я делаю dispatch только тогда, когда мне пришел ответ от сервера?