Redux: общее состояние приложения
- Зачем это нужно
- Цена протаскивания через уровни
- Идея: одно состояние, один путь его менять
- Хранилище работает и без React
- Действие: объект с именем
- Обработчик: обычная функция
- «Меняем на месте», а старое цело
- Когда хранилище нужно, а когда нет
- Сколько это стоит
- Типичные ошибки
- Слова главы
- Тренажёр: набрать вслепую
- На чемпионате
- Куда углубиться
- Куда дальше: проверенные ресурсы
Зачем это нужно
Подключённый кошелёк нужен шапке, форме вклада и списку операций. Список операций нужен и списку, и счётчику в шапке. Признак «идёт транзакция» — кнопке, полоске состояния и блокировке формы.
Пока значений три и экран один, всё решается подъёмом состояния к общему родителю. Как только экранов становится пять, общий родитель оказывается корнем приложения — и значения приходится передавать через все уровни вниз.
Эта глава — про то, во что это обходится, и про другой способ: одно хранилище, из которого каждый берёт то, что ему нужно.
Цена протаскивания через уровни
Прогон строит два одинаковых дерева из семи компонентов. В первом значение идёт свойствами сверху вниз, во втором его берут из хранилища прямо там, где оно нужно. Меняем значение и считаем, сколько функций вызвалось заново:
── значение идёт свойствами через пять уровней ──
App: 1, Sidebar: 1, L1: 1, L2: 1, L3: 1, L4: 1, L5: 1
всего вызовов: 7 на экране: 2.5
── значение берут из хранилища там, где оно нужно ──
L5: 1
всего вызовов: 1 на экране: 2.5
Обратите внимание на Sidebar: меню не имеет к сумме никакого отношения, но пересчиталось вместе со всеми, потому что его родитель пересчитался.
Дело не столько в скорости — семь вызовов ничего не стоят. Дело в том, что промежуточные компоненты вынуждены знать про значение, которое им не нужно: принимать его, объявлять в типах и передавать дальше. Через полгода никто не может убрать свойство, потому что непонятно, кому оно нужно.
Не всякое общее значение требует хранилища. Для «редко меняющегося и всем нужного» — темы оформления, языка, адреса контракта — в React есть встроенный способ раздать значение вглубь без передачи через уровни. Хранилище нужно там, где значение часто меняется и меняют его из разных мест.
Идея: одно состояние, один путь его менять
Три правила, из которых состоит вся библиотека:
- Состояние приложения — один объект. Не разбросано по компонентам, а лежит в одном месте.
- Менять его напрямую нельзя. Можно только отправить действие — объект, описывающий, что произошло.
- Новое состояние вычисляет обычная функция от старого состояния и действия.
Из третьего правила следует главное свойство: всё, что происходило в приложении, описано списком действий. Сохранили список — можете воспроизвести состояние; показали список — видно, что делал пользователь.
Хранилище работает и без React
Важное, что теряется при быстром знакомстве: библиотека хранилища не имеет отношения к React. Прогон создаёт хранилище и работает с ним напрямую:
── хранилище живёт само по себе ──
начальное состояние: {"amount":"0","ops":[],"busy":false}
── что записалось, шаг за шагом ──
1) {"amount":"2.5","ops":[],"busy":false}
2) {"amount":"2.5","ops":[{"hash":"0xabc","amount":"2.5"}],"busy":false}
3) {"amount":"2.5","ops":[{"hash":"0xabc","amount":"2.5"}],"busy":true}
const store = configureStore({ reducer: { vault: vault.reducer } });
store.getState(); // прочитать
store.subscribe(() => { … }); // узнавать об изменениях
store.dispatch(vault.actions.setAmount("2.5")); // изменить
Три шага — три записанных состояния. Это и есть история, о которой шла речь.
Действие: объект с именем
── что такое действие ──
{"type":"vault/setAmount","payload":"7"}
имя действия: vault/setAmount
Действие — обычный объект с обязательным полем-именем и, обычно, полезной нагрузкой. Имя складывается из названия раздела и названия действия; в списке отправленных действий такие имена читаются как рассказ о происходившем.
Имена пишут от лица события, а не команды: не «поставить сумму», а «пользователь ввёл сумму»; не «добавить операцию», а «транзакция подтверждена». Разница проявляется, когда на одно событие должны отреагировать два раздела: команда адресована одному, событие — всем, кому интересно.
Обработчик: обычная функция
── обработчик — обычная функция от состояния и действия ──
одинаковый вход два раза → одинаковый выход: true
результат: {"amount":"9","ops":[],"busy":false}
Обработчик — функция (состояние, действие) → новое состояние. Никакой магии: её можно вызвать руками, как в прогоне, и проверить.
Из этого следуют два практических вывода.
Обработчики — самое простое место для тестов. Ни сети, ни браузера, ни React: вход и выход.
В обработчике не бывает запросов, таймеров и случайных чисел. Он обязан быть вычислением — по тем же причинам, по которым им обязан быть компонент. Куда девать всё остальное — тема следующих двух глав.
«Меняем на месте», а старое цело
В обработчике пишут так, будто состояние меняют напрямую:
reducers: {
setAmount: (s, a) => { s.amount = a.payload; },
addOp: (s, a) => { s.ops.push(a.payload); },
}
Это выглядит нарушением всех правил. Прогон проверяет, что произошло на самом деле:
── старое состояние не тронуто ──
прежний объект целиком: {"amount":"0","ops":[],"busy":false}
это тот же объект? : false
массив операций тот же? : false
(а ведь в обработчике написано ops.push — но исходный массив остался пустым)
Библиотека передаёт в обработчик не само состояние, а черновик, который записывает все обращения. По окончании из черновика собирается новый объект — старый остаётся нетронутым.
Зачем такая сложность: сравнение по ссылке. Раз при любом изменении получается новый объект, React достаточно сравнить ссылки, чтобы понять, изменилось что-то или нет. Это то же правило, что в главе про состояние: массив, изменённый на месте, не вызывает перерисовки.
Черновик работает только внутри обработчика. Значение, полученное из хранилища в компоненте, менять нельзя ни в коем случае: это настоящее состояние, и его изменение не заметит никто — ни React, ни сама библиотека.
Когда хранилище нужно, а когда нет
Нужно: значение меняется часто и меняется из разных мест. Подключённый кошелёк, список операций, признак идущей транзакции, выбранная сеть.
Не нужно: значение живёт внутри одного компонента. Открыт ли выпадающий список, что набрано в поле, какая вкладка выбрана. Класть такое в общее хранилище — значит превратить простую вещь в три файла.
Спорно: данные, приходящие из сети. Их состояние (загрузка, ошибка, значение) отлично ложится в хранилище, но для этого есть отдельные средства — о них в последней главе блока.
Сколько это стоит
без хранилища : 222.53 кБ │ gzip: 69.28 кБ
с хранилищем : 242.99 кБ │ gzip: 77.13 кБ
Двадцать килобайт, восемь при передаче — вдвое меньше, чем маршрутизатор. За это вы получаете хранилище, обработчики, черновики, связь с React и средства отладки.
Типичные ошибки
Кладут в хранилище всё. Включая открытые выпадающие списки. Проект обрастает действиями на каждое движение мыши.
Меняют то, что пришло из хранилища, в компоненте. Изменение не увидит никто, а отладка займёт час.
В обработчике делают запрос. Обработчик обязан быть вычислением.
Хранят в состоянии вычислимое. Сумму, которую можно посчитать по списку. Однажды они разойдутся.
Действия называют командами. «Установить сумму» вместо «пользователь ввёл сумму»: на одно событие реагировать двум разделам становится неудобно.
Берут из хранилища весь объект целиком. Компонент начинает пересчитываться на любое изменение чего угодно — и выигрыш, ради которого всё затевалось, исчезает.
Заводят хранилище там, где хватило бы подъёма состояния. Три компонента и одно значение — это не тот случай.
Слова главы
- Протаскивание через уровни — передача значения свойствами сквозь компоненты, которым оно не нужно.
- Хранилище — один объект состояния на всё приложение; работает и без React.
- Действие — объект с именем и нагрузкой; единственный способ сообщить об изменении.
- Обработчик — чистая функция от состояния и действия к новому состоянию.
- Отправка действия — синхронна: после возврата новое состояние уже на месте.
- Подписка — вызывается после каждого применённого действия.
- Черновик — то, что приходит в обработчик; из него собирается новый объект.
- Сравнение по ссылке — почему неизменяемость важна: новый объект виден сразу.
Тренажёр: набрать вслепую
Цель: скорость ≥ 95 зн/мин, точность ≥ 88%
const store = configureStore({ reducer: { vault: vault.reducer } });
На чемпионате
Не начинайте с хранилища. Начните с подъёма состояния. Хранилище добавляют тогда, когда протаскивание через уровни начинает мешать, — обычно на третьем-четвёртом экране.
Разделов немного и по смыслу. Кошелёк, операции, форма. Не «один раздел на компонент».
Имена действий читаемы. На защите список отправленных действий — готовый рассказ о том, что делал пользователь.
Обработчики покройте тестами. Это самое дешёвое тестирование во всём фронтенде: вход, выход, ни одной подделки.
И честно: для интерфейса из четырёх экранов хранилище — не обязательное требование. Оно становится выгодным, когда состояние действительно общее. Задание его упоминает, и знать его надо; применять — по делу.
Куда углубиться
- Средства отладки в браузере — список действий, состояние на каждом шаге, переход назад по истории.
- Промежуточные обработчики — как действие проходит через цепочку перед тем, как попасть в обработчик.
- Встроенный способ раздать значение вглубь — чем он отличается от хранилища и когда его достаточно.
- Нормализация данных — почему список операций хранят как карту по идентификатору.
- Сохранение состояния между заходами — и чего в него класть нельзя.
Сводная проверка по всей главе: 8 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.
Куда дальше: проверенные ресурсы
- Redux: основы — идея, действия, обработчики.
- Redux: три принципа — те самые три правила.
- Redux Toolkit: почему он — что именно он упрощает.
- Immer — библиотека черновиков, работающая внутри обработчиков.
- React: передача данных вглубь без свойств — когда хранилище не нужно.
Комментарии
Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.