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 — Чистые редьюсеры обязаны возвращать новое состояние иммутабельно, не мутируя прежнее