State Management

Redux: ядро

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

  • Корзина и каталог в e-commerce: цена, количество, скидки в одном дереве состояния, видимом из любого компонента
  • Сложные дашборды с фильтрами, где фильтр в одном месте должен синхронно влиять на несколько панелей
  • Приложения с историей действий: undo/redo через журнал отправленных action
  • Команды, которым важна воспроизводимость бага: лог action позволяет повторить точную последовательность
  • Крупные приложения, где десятки разработчиков должны одинаково понимать, как меняется состояние

Предварительные знания

  • Flux: однонаправленный поток action - dispatcher - store - view
  • Редьюсер как чистая функция (state, action) и возврат нового состояния
  • Иммутабельное обновление: создание нового объекта вместо мутации старого
  • Flux и редьюсер

Откуда взялся Redux

Redux представили Дэн Абрамов и Эндрю Кларк в 2015 году на конференции React Europe. Идея выросла из архитектуры Flux от Facebook, но упростила её до одного store и чистых редьюсеров вместо множества store с обратными вызовами. Толчком стала разработка инструмента отладки с путешествием во времени: чтобы перематывать состояние вперёд и назад, переходы должны быть детерминированными чистыми функциями, а всё состояние храниться в одном месте. Это и зафиксировало три принципа. Redux быстро стал стандартом управления состоянием в React-экосистеме на годы вперёд, пока позже Redux Toolkit не убрал большую часть его ручной обвязки.

Store, action, reducer, dispatch

В Redux четыре понятия работают вместе. Store это единый объект, хранящий всё дерево состояния приложения. Action это обычный объект, описывающий, что произошло: у него есть поле type и при необходимости полезная нагрузка. Reducer это чистая функция, которая по текущему состоянию и пришедшему action вычисляет новое состояние. Dispatch это способ отправить action в store, чтобы запустить пересчёт состояния через reducer.

Обратить внимание стоит на default-ветку: при неизвестном action reducer обязан вернуть состояние без изменений. Это важно, потому что store при инициализации прогоняет служебный action, и reducer должен отдать начальное значение. Полезная нагрузка лежит в поле payload, а не разбросана по корню action: так store devtools и логи остаются единообразными.

ПонятиеЧто этоРоль
StoreОбъект с деревом состоянияХранит состояние, отдаёт его через getState
ActionОбъект { type, payload? }Описывает что произошло, без логики
ReducerЧистая функция (state, action)Вычисляет новое состояние
DispatchМетод storeОтправляет action и запускает reducer

Что обязан вернуть reducer, если пришёл action с неизвестным ему type?

Однонаправленный поток данных

Главная архитектурная идея Redux в том, что данные текут в одну сторону. Компонент отправляет action через dispatch. Store передаёт текущее состояние и этот action в reducer. Reducer возвращает новое состояние. Store сохраняет его и оповещает подписчиков, и UI перерисовывается с новыми данными. Замкнуть круг можно только новым action. Нет пути, по которому компонент менял бы состояние напрямую, минуя этот цикл.

Ценность однонаправленности в предсказуемости. Раз состояние меняется только через action, любое изменение оставляет след в виде отправленного action. Можно записать всю их последовательность в лог, повторить её и получить ровно то же конечное состояние. Двунаправленная связь, где UI и состояние правят друг друга в обе стороны, такой гарантии не даёт: источник изменения теряется.

Именно однонаправленный детерминированный поток делает возможным devtools с путешествием во времени. Инструмент хранит список action и начальное состояние, а раз reducer чист, любую точку истории можно восстановить, проиграв action заново. Без чистоты редьюсера и единого store это не работает.

Почему однонаправленный поток данных в Redux упрощает отладку?

Три принципа Redux

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

  1. Единый источник истины: одно дерево состояния в одном store на всё приложение
  2. Состояние только для чтения: изменить его можно лишь через dispatch(action)
  3. Изменения через чистые редьюсеры: (state, action) без побочных эффектов и без мутации входа

Нарушение принципа чистоты ломает Redux тихо. Если редьюсер сделает state.items.push(...) и вернёт тот же объект, ссылка на состояние не изменится. Многие оптимизации сравнивают состояние по ссылке, поэтому UI может не обновиться, а devtools покажет неверную историю. Поэтому в чистом Redux каждое изменение это создание нового объекта.

Ровно эта ручная иммутабельность через спред-операторы и есть та боль, которую позже убирает Redux Toolkit. В RTK редьюсер можно писать в мутирующем стиле, а Immer под капотом превращает это в корректное создание нового состояния. Но понимать исходное правило важно: внизу всё равно иммутабельность.

Какой из трёх принципов Redux нарушает редьюсер, который делает state.items.push(item) и возвращает тот же объект state?

Зачем Redux появился

До Redux общее состояние в крупных приложениях растекалось по компонентам и сервисам. Одни и те же данные жили в нескольких местах и расходились. Источник изменения было трудно найти: состояние мог поменять любой обработчик в любом компоненте. Архитектура Flux от Facebook предложила однонаправленный поток, но оставляла несколько store с перекрёстными зависимостями. Redux упростил это до одного store и чистых редьюсеров.

  • Состояние без Redux — Данные дублируются по компонентам, меняются откуда угодно, расходятся между собой. Найти источник изменения и воспроизвести баг тяжело
  • Состояние с Redux — Одно дерево, изменения только через action, переходы чисты. Любое изменение прослеживается, история повторяема, баг воспроизводится по логу

Redux покупает предсказуемость ценой обвязки. За неё платят явными типами action, отдельными редьюсерами и ручной иммутабельностью. Этот объём кода и стал главной критикой раннего Redux. Ответ на критику это Redux Toolkit: он сохраняет ядро и три принципа, но генерирует action и reducer из описания slice и берёт иммутабельность на себя через Immer.

Важно держать в голове границу применимости. Redux оправдан там, где предсказуемость общего состояния стоит дороже краткости: крупные приложения, сложный поток, командная разработка, требование воспроизводить баги. Для маленького локального состояния его обвязка избыточна, и это тема отдельного урока про паттерны и уместность Redux.

Какую проблему Redux ставил во главу угла при своём появлении?

Связь с другими темами

Ядро Redux это фундамент, поверх которого строится всё остальное:

  • Flux и редьюсер — Redux это упрощение Flux: один store вместо многих и чистый редьюсер как единственный способ перехода
  • Redux Toolkit: slices и Immer — RTK сохраняет это ядро, но прячет ручной boilerplate: createSlice генерирует action и reducer за разработчика

Итог

  • Redux это контейнер состояния с однонаправленным потоком: action описывает что произошло, reducer вычисляет новое состояние, store хранит его
  • Единственный способ изменить состояние это отправить action через dispatch, прямая мутация запрещена
  • Reducer это чистая функция (state, action) без побочных эффектов: на одинаковый вход всегда одинаковый выход
  • Три принципа: единый источник истины (один store), состояние только для чтения, изменения только через чистые редьюсеры
  • Детерминированность переходов это то, что делает возможными devtools с путешествием во времени и воспроизведение багов по логу action
  • Redux появился как упрощение Flux и зафиксировал предсказуемость состояния как главную ценность

Связанные уроки

  • sm-07-flux-reducer — Flux и идея редьюсера это прямой предшественник Redux: store-dispatcher-view и чистая функция перехода состояния
  • sm-12-rtk-slices — Поняв ядро Redux, следующий шаг это Redux Toolkit, который убирает ручной boilerplate этого ядра
Redux: ядро

0

1

Войти