State Management

Иммутабельность и нормализация

Компонент списка не перерисовался, хотя задача в нём изменилась. Причина: код поменял поле напрямую - task.done = true - и ссылка на массив осталась прежней. Стор сравнивает старое и новое состояние по ссылке, видит ту же ссылку и делает вывод: ничего не поменялось, рендерить не нужно. UI замер с устаревшими данными. Иммутабельные обновления решают это: каждое изменение создаёт новую ссылку, и сравнение по ссылке честно сообщает, что обновить. А нормализация решает родственную проблему: одна и та же сущность не должна лежать копиями в десяти вложенных местах.

  • Список задач, где смена одного поля должна вызвать ре-рендер именно изменившейся строки
  • Лента постов с авторами: автор хранится один раз по id, а не копией в каждом посте
  • Глубоко вложенный объект настроек, обновление которого без мутаций иначе превращается в лесенку спредов
  • Чат, где сообщение и его автор нормализованы по id, чтобы смена имени отразилась везде сразу
  • Корзина и каталог, ссылающиеся на товар по id вместо двух разъезжающихся копий

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

  • Принцип единого источника истины и идея хранить ссылку, а не копию
  • Понимание разницы между сравнением по ссылке и по значению
  • Базовое знакомство со спред-синтаксисом объектов и массивов в JavaScript
  • Единый источник истины

Иммутабельные обновления

Иммутабельное обновление означает, что прежний объект состояния не меняется на месте, а вместо него создаётся новый с применёнными правками. Старое состояние остаётся нетронутым. Это даёт два преимущества: предсказуемость, поскольку существующие данные никто незаметно не перезапишет, и дешёвое обнаружение изменений по ссылке.

Почему обнаружение изменений зависит от ссылок. Стор и UI-фреймворк определяют, надо ли перерисовывать, сравнивая прошлое и новое значение через проверку ссылки (===). Это очень быстро, в отличие от глубокого сравнения всех вложенных полей. Но работает только при иммутабельности: если каждое изменение даёт новую ссылку, равенство ссылок надёжно означает нет изменений.

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

Почему прямая мутация task.done = true может привести к тому, что список не перерисуется?

Нормализация состояния

Нормализация это хранение данных плоско: каждая сущность лежит ровно один раз в словаре по своему id, а связи выражаются ссылками-id, а не вложенными копиями. Это переносит принцип единого источника истины на структуру данных. Глубокая вложенность с дублированием сущностей приводит к разъезжающимся копиям и громоздким иммутабельным обновлениям.

СвойствоГлубокая вложенностьНормализованная форма
Дубли сущностейЕсть, копии расходятсяНет, одна копия по id
Обновление сущностиНайти все вхожденияПравка одной записи byId
Иммутабельное обновлениеДлинная цепочка спредовТочечно по id, мелко
Поиск по idОбход вложенностиПрямой доступ byId[id]

Типичная нормализованная форма: словарь byId (сущность по идентификатору) плюс массив allIds для порядка. Списки тогда хранят массивы id, а сами объекты берутся из byId. Этот приём широко применяется в Redux Toolkit через createEntityAdapter.

В чём основная выгода нормализации состояния по id?

Immer: мутативный синтаксис, иммутабельный результат

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

Оба варианта дают один результат: новый объект с новыми ссылками на изменённом пути и прежними ссылками на нетронутых ветках. Immer лишь делает запись короче и читаемее, особенно на глубоких структурах. Поэтому он встроен в Redux Toolkit: редьюсеры там можно писать с виду мутативно, а на деле они остаются иммутабельными.

draft внутри produce это особый proxy-объект, существующий только на время функции. Менять его можно как угодно, но возвращать наружу или сохранять его не следует: наружу отдаётся результат produce, а не сам draft.

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

Что делает Immer, когда внутри produce код выглядит как прямая мутация draft?

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

Иммутабельность и нормализация лежат в основе reducer-модели и многих сторов:

  • Flux и reducer-модель — Чистый редьюсер возвращает новое состояние иммутабельно, никогда не мутируя прежнее
  • Единый источник истины — Нормализация это тот же принцип: каждая сущность хранится в одном каноническом месте по id

Итог

  • Иммутабельное обновление не меняет прежний объект, а создаёт новый с нужными правками
  • Новая ссылка позволяет дёшево обнаруживать изменения сравнением по ссылке вместо глубокого обхода
  • Прямая мутация оставляет ту же ссылку, поэтому ре-рендер не срабатывает и UI застывает на старых данных
  • Нормализация хранит сущности плоско по id, а не копиями в глубокой вложенности
  • Нормализованная форма убирает дубли сущностей: правка по id видна везде, где на неё ссылаются
  • Immer позволяет писать код как мутацию, а под капотом получает корректное иммутабельное обновление

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

  • sm-03-single-source-of-truth — Нормализация хранит каждую сущность ровно один раз - это единый источник истины на уровне структуры данных
  • sm-07-flux-reducer — Чистые редьюсеры обязаны возвращать новое состояние иммутабельно, не мутируя прежнее
Иммутабельность и нормализация

0

1

Войти