State Management

Производное состояние

Экран показывает только активные задачи и их число. Разработчик заводит поле activeTasks, рядом activeCount, и обновляет оба при каждом действии: добавил задачу, выполнил, удалил, сменил фильтр. Полтора десятка мест, где легко забыть пересчёт. Стоит понять, что и список активных, и их число это просто функция от полного списка задач и текущего фильтра - и поля исчезают. Остаётся один источник (все задачи плюс фильтр), а активные и их количество выводятся из него на каждом рендере.

  • Список активных задач из полного списка и текущего фильтра
  • Число непрочитанных уведомлений как количество элементов с флагом unread
  • Итоговая цена со скидкой из позиций корзины и промокода
  • Отсортированная и отфильтрованная таблица из исходных строк и параметров вида
  • Признак валидности формы как результат проверки всех полей, а не отдельный флаг

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

  • Принцип единого источника истины из предыдущего урока
  • Идея хранить входы, выводить результаты
  • Базовое знакомство с функциями высшего порядка (map, filter, reduce)
  • Единый источник истины

Не хранить вычислимое

Производное состояние это любое значение, которое можно получить из уже имеющегося состояния чистой функцией. Список активных задач выводится из всех задач и фильтра. Число непрочитанных выводится из списка уведомлений. Раз значение определяется входами однозначно, хранить его как отдельное поле - значит снова создавать дубль и обязанность синхронизировать. Вместо этого его вычисляют там, где оно нужно.

Теперь добавление, выполнение или удаление задачи меняет только tasks. Активный список и его число пересчитываются сами при следующем чтении, потому что они производные. Полей для ручной синхронизации не осталось, и рассинхрон между числом и списком стал невозможен в принципе.

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

Почему список активных задач не стоит хранить отдельным полем рядом с полным списком?

Селекторы и computed-значения

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

В разных экосистемах та же идея называется по-разному. В Redux это селекторы, в Zustand - функции-аргументы хука стора, в Vue и MobX - computed-значения, в Jotai - производные атомы. Суть одна: объявить значение как функцию от другого состояния, а не как отдельно хранимое поле.

  • Селектор (производное значение) — selectTotal выводит сумму из позиций и скидки. Меняется позиция - total автоматически другой при следующем чтении. Хранить нечего, синхронизировать нечего.
  • Хранимое поле total — total лежит в сторе и требует ручного пересчёта при любой правке позиций или скидки. Забыли пересчитать - показали устаревшую сумму.

Селекторы удобно держать рядом с определением стора и переиспользовать. Это и единое место правды для логики вывода, и точка, куда позже добавляется мемоизация, если вычисление окажется дорогим.

Что такое селектор в контексте производного состояния?

Мемоизация: когда она оправдана

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

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

СитуацияМемоизацияПочему
Чтение одного поля примитиваНе нужнаСравнение по значению, вычисления нет
Сумма по короткому массивуОбычно не нужнаДёшево пересчитать на каждом рендере
Сортировка/фильтр больших списковОправданаДорогая операция, выгодно кэшировать
Селектор возвращает новый объект/массивОправданаСтабилизирует ссылку, убирает лишние ре-рендеры

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

В каком случае мемоизация селектора оправдана?

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

Производное состояние реализуется через механизмы, которые встречаются в каждом сторе:

  • Селекторы Zustand — Селектор это функция, выводящая нужный кусок из стора, в том числе производный
  • computed в Pinia — Геттеры-вычисления в Pinia это та же идея производного значения с кэшированием

Итог

  • Производное состояние это значение, полностью определяемое другим состоянием, поэтому его вычисляют, а не хранят
  • Селектор это функция (state) => результат, которая выводит нужный кусок, в том числе производный
  • Вычисление на каждом рендере убирает целый класс багов рассинхрона: производное не может разойтись с источником
  • Мемоизация кэширует результат и пересчитывает его только при изменении входов селектора
  • Мемоизация оправдана для дорогих вычислений (сортировка, фильтрация больших списков) или когда результат - новая ссылка
  • Преждевременная мемоизация дешёвых вычислений добавляет сложность без выгоды: сперва измерить, потом кэшировать

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

  • sm-03-single-source-of-truth — Производное состояние это практическое следствие правила хранить только неустранимое
  • rc-38-zustand-state — Селекторы Zustand это и есть способ читать производные значения без лишних ре-рендеров
  • vue-27-pinia-intro — computed-геттеры в Pinia это та же идея производного состояния в другой экосистеме
Производное состояние

0

1

Войти