Перейти к основному содержимому
Блокчейн48ГЛАВА 48Redux: общее состояние приложенияstoreactionreducer7 → 1 вызов

Redux: общее состояние приложения

Прочитано0%
Квизы0/4
Тренажёрыне пройден0/1
Уровень 1 · Новичок

Зачем это нужно​

Подключённый кошелёк нужен шапке, форме вклада и списку операций. Список операций нужен и списку, и счётчику в шапке. Признак «идёт транзакция» — кнопке, полоске состояния и блокировке формы.

Пока значений три и экран один, всё решается подъёмом состояния к общему родителю. Как только экранов становится пять, общий родитель оказывается корнем приложения — и значения приходится передавать через все уровни вниз.

Эта глава — про то, во что это обходится, и про другой способ: одно хранилище, из которого каждый берёт то, что ему нужно.

Цена протаскивания через уровни​

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

── значение идёт свойствами через пять уровней ──
App: 1, Sidebar: 1, L1: 1, L2: 1, L3: 1, L4: 1, L5: 1
всего вызовов: 7 на экране: 2.5

── значение берут из хранилища там, где оно нужно ──
L5: 1
всего вызовов: 1 на экране: 2.5
ОДНО ЗНАЧЕНИЕ ИЗМЕНИЛОСЬ: СКОЛЬКО ФУНКЦИЙ ВЫЗВАНО ЗАНОВОзначение идёт свойствами через пять уровнейAppSidebarL1L2L3L4L5всего вызовов: 7 — включая меню, которому это значение не нужно вовсезначение берут из хранилища там, где оно нужноAppSidebarL1L2L3L4L5всего вызовов: 1 — только тот компонент, который это значение читаетна экране в обоих случаях одно и то же; разница в том, сколько работы ради этого сделано
Семь вызовов против одного; на экране при этом одно и то же

Обратите внимание на Sidebar: меню не имеет к сумме никакого отношения, но пересчиталось вместе со всеми, потому что его родитель пересчитался.

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

Интересный факт

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

Идея: одно состояние, один путь его менять​

Три правила, из которых состоит вся библиотека:

  1. Состояние приложения — один объект. Не разбросано по компонентам, а лежит в одном месте.
  2. Менять его напрямую нельзя. Можно только отправить действие — объект, описывающий, что произошло.
  3. Новое состояние вычисляет обычная функция от старого состояния и действия.
КРУГ, ПО КОТОРОМУ ХОДЯТ ВСЕ ИЗМЕНЕНИЯдействиеобъект с именем{ type: "vault/setAmount", payload: "7" }обработчикобычная функция(состояние, действие)→ новое состояниехранилищедержит текущееgetState()dispatch()подписчикиузнают об измененииsubscribe()перерисовкасостояние меняется только так: другого пути внутрь хранилища нетпоэтому по списку отправленных действий видно всю историю того, что происходило в приложении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: вход и выход.

В обработчике не бывает запросов, таймеров и случайных чисел. Он обязан быть вычислением — по тем же причинам, по которым им обязан быть компонент. Куда девать всё остальное — тема следующих двух глав.

Отвечено вопросов: 0 из 2
Значение прошло свойствами через пять уровней — пересчиталось семь компонентов вместо одного. В чём главная беда?
Что такое действие?
Отвечено верно: 0 из 2

«Меняем на месте», а старое цело​

В обработчике пишут так, будто состояние меняют напрямую:

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 — но исходный массив остался пустым)
В ОБРАБОТЧИКЕ НАПИСАНО ops.push — А СТАРОЕ СОСТОЯНИЕ ЦЕЛОсостояние до трёх действий{ amount: "0", ops: [], busy: false }после всех действий — ровно такое жеобъект не тронут, массив пустсостояние после{ amount: "2.5", ops: [{…}], busy: true }это тот же объект? falseтот же массив ops? falseзапись «изменяем на месте» — обман зрения: библиотека отдаёт черновик и собирает из него новый объектблагодаря этому сравнение по ссылке работает — и React видит, что изменилось, не сверяя поля
Запись «изменяем на месте» — обман зрения: из черновика собирается новый объект

Библиотека передаёт в обработчик не само состояние, а черновик, который записывает все обращения. По окончании из черновика собирается новый объект — старый остаётся нетронутым.

Зачем такая сложность: сравнение по ссылке. Раз при любом изменении получается новый объект, React достаточно сравнить ссылки, чтобы понять, изменилось что-то или нет. Это то же правило, что в главе про состояние: массив, изменённый на месте, не вызывает перерисовки.

Важно

Черновик работает только внутри обработчика. Значение, полученное из хранилища в компоненте, менять нельзя ни в коем случае: это настоящее состояние, и его изменение не заметит никто — ни React, ни сама библиотека.

Когда хранилище нужно, а когда нет​

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

Не нужно: значение живёт внутри одного компонента. Открыт ли выпадающий список, что набрано в поле, какая вкладка выбрана. Класть такое в общее хранилище — значит превратить простую вещь в три файла.

Спорно: данные, приходящие из сети. Их состояние (загрузка, ошибка, значение) отлично ложится в хранилище, но для этого есть отдельные средства — о них в последней главе блока.

Сколько это стоит​

без хранилища : 222.53 кБ │ gzip: 69.28 кБ
с хранилищем : 242.99 кБ │ gzip: 77.13 кБ

Двадцать килобайт, восемь при передаче — вдвое меньше, чем маршрутизатор. За это вы получаете хранилище, обработчики, черновики, связь с React и средства отладки.

Отвечено вопросов: 0 из 2
В обработчике написано ops.push — а исходный массив остался пустым. Почему?
Что НЕ стоит класть в общее хранилище?
Отвечено верно: 0 из 2

Типичные ошибки​

Кладут в хранилище всё. Включая открытые выпадающие списки. Проект обрастает действиями на каждое движение мыши.

Меняют то, что пришло из хранилища, в компоненте. Изменение не увидит никто, а отладка займёт час.

В обработчике делают запрос. Обработчик обязан быть вычислением.

Хранят в состоянии вычислимое. Сумму, которую можно посчитать по списку. Однажды они разойдутся.

Действия называют командами. «Установить сумму» вместо «пользователь ввёл сумму»: на одно событие реагировать двум разделам становится неудобно.

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

Заводят хранилище там, где хватило бы подъёма состояния. Три компонента и одно значение — это не тот случай.

Отвечено вопросов: 0 из 2
Нужен ли React, чтобы пользоваться хранилищем?
Почему в обработчике не бывает запросов, таймеров и случайных чисел?
Отвечено верно: 0 из 2

Слова главы​

  • Протаскивание через уровни — передача значения свойствами сквозь компоненты, которым оно не нужно.
  • Хранилище — один объект состояния на всё приложение; работает и без React.
  • Действие — объект с именем и нагрузкой; единственный способ сообщить об изменении.
  • Обработчик — чистая функция от состояния и действия к новому состоянию.
  • Отправка действия — синхронна: после возврата новое состояние уже на месте.
  • Подписка — вызывается после каждого применённого действия.
  • Черновик — то, что приходит в обработчик; из него собирается новый объект.
  • Сравнение по ссылке — почему неизменяемость важна: новый объект виден сразу.

Тренажёр: набрать вслепую​

Цель: скорость ≥ 95 зн/мин, точность ≥ 88%

const store = configureStore({ reducer: { vault: vault.reducer } });

На чемпионате​

Не начинайте с хранилища. Начните с подъёма состояния. Хранилище добавляют тогда, когда протаскивание через уровни начинает мешать, — обычно на третьем-четвёртом экране.

Разделов немного и по смыслу. Кошелёк, операции, форма. Не «один раздел на компонент».

Имена действий читаемы. На защите список отправленных действий — готовый рассказ о том, что делал пользователь.

Обработчики покройте тестами. Это самое дешёвое тестирование во всём фронтенде: вход, выход, ни одной подделки.

И честно: для интерфейса из четырёх экранов хранилище — не обязательное требование. Оно становится выгодным, когда состояние действительно общее. Задание его упоминает, и знать его надо; применять — по делу.

Куда углубиться​

  • Средства отладки в браузере — список действий, состояние на каждом шаге, переход назад по истории.
  • Промежуточные обработчики — как действие проходит через цепочку перед тем, как попасть в обработчик.
  • Встроенный способ раздать значение вглубь — чем он отличается от хранилища и когда его достаточно.
  • Нормализация данных — почему список операций хранят как карту по идентификатору.
  • Сохранение состояния между заходами — и чего в него класть нельзя.

Сводная проверка по всей главе: 8 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.

Куда дальше: проверенные ресурсы​

Видео по теме

Видео1. Redux и React. Все о Redux. Понятная теория для всех. React + redux.Ulbi TV
ВидеоВсе что нужно знать о Redux - action, dispatch, reducer, storeВеб-разработка - DevMagazine
ВидеоОбъясняя Redux за 5 минутАлексей Волков про JavaScript
ВидеоRedux простым языкомДмитрий Герасимов - о разработке в сфере IT просто
ВидеоRedux ЛУЧШЕ useContext? Это МИФ!Алексей Волков про JavaScript

Комментарии

Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.