Локальное и общее состояние

Что это такое

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

Почему это важно

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

Ключевые идеи

Начинайте локально и поднимайте только при необходимости

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

// Локально: больше никто не может его увидеть или инвалидировать
const [open, setOpen] = useState(false);

Триггер для подъёма — второй читатель, а не ощущение, что значение «важное».

Подъём состояния — это перенос владельца, а не копирование

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

function Page() {
  const [query, setQuery] = useState("");
  return (
    <>
      <SearchField value={query} onChange={setQuery} />
      <ResultCount query={query} />
    </>
  );
}

Хранилище заслуживает место, когда оно сквозное

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

Разным задачам подходят разные формы

Единое хранилище с действиями и редьюсерами делает каждое изменение отслеживаемым — это подходит приложениям со сложным состоянием, которое нужно аудировать. Для простых случаев легче маленькое хранилище с прямыми сеттерами. Реактивные примитивы, то есть сигналы, позволяют фреймворку отслеживать зависимости через обычные значения и вовсе обойтись без хранилища. Названия различаются; компромисс между отслеживаемостью, церемонией и связанностью — нет.

Держите правило обновления в одном месте

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

Практика

Верните значение обратно в локальное состояние

Найдите в глобальном хранилище значение, которое читает только один экран. Перенесите его в локальное состояние этого экрана и удалите из хранилища вместе с правилами сброса при размонтировании.

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

Поднимите значение к ближайшему общему владельцу

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

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

Частые ошибки

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

Что читать дальше

  • JavaScript (02-frontend/06-javascript) — замыкания и идентичность объектов — то, из-за чего локальное и общее состояние ведут себя по-разному.
  • Клиентское и серверное состояние (02-frontend/23-state-management/02-client-and-server-state) — следующий вопрос: наше ли это значение вообще?
  • React (02-frontend/29-react) — правила состояния и подъёма, которые обобщает этот урок.