State Management
Единый источник истины
В сторе лежит массив items, рядом поле totalCount, а ещё isEmpty. Три значения про одно и то же. Разработчик добавляет товар в items, забывает поправить totalCount, и счётчик в шапке врёт. Чинит счётчик - забывает про isEmpty, и пустой экран показывается поверх непустого списка. Каждое продублированное значение это новый шанс на рассинхрон. Единый источник истины убирает саму возможность ошибки: items хранится, а totalCount и isEmpty выводятся из него и поэтому не могут разойтись.
- Список и его длина: длину выводят из списка, а не хранят рядом
- Полное имя из firstName и lastName: вычисляется, а не дублируется третьим полем
- Признак пустоты корзины: производный от наличия товаров, отдельно не хранится
- Выбранный элемент: хранят его id, а не копию всего объекта, который уже лежит в списке
- Итоговая сумма заказа: считается из позиций, иначе при правке позиции она устаревает
Предварительные знания
- Понимание категорий состояния из предыдущего урока
- Идея рассинхрона из дублирования данных
- Базовое умение писать функции, возвращающие значение из входных данных
Дублирование порождает баги
Когда одно и то же знание хранится в двух полях, появляется обязанность держать их согласованными при каждом изменении. Эту обязанность легко забыть, и тогда поля расходятся. Чем больше дублей, тем больше путей рассинхрона. Поэтому борьба за корректность интерфейса во многом сводится к сокращению числа независимо хранимых копий.
Поле count это длина items. Поле isEmpty это проверка items на пустоту. Оба полностью определяются массивом items, значит хранить их отдельно - значит вручную поддерживать инвариант, который машина могла бы поддерживать сама. Любая забытая правка ломает согласованность.
Дублирование не всегда выглядит как точная копия. Производные значения (длина, сумма, отсортированный вариант списка, флаг наличия) это тоже дубли: они повторяют информацию, уже содержащуюся в источнике, и точно так же способны разойтись.
Почему хранение count рядом с массивом items считается дублированием?
Одно каноническое место
Единый источник истины означает, что у каждого факта есть ровно одно место, где он хранится, и все, кому он нужен, читают именно оттуда. Если выбранный товар уже лежит в списке items, не стоит хранить его копию в отдельном поле selectedItem - достаточно хранить selectedId и брать сам объект из списка. Тогда правка товара в списке мгновенно отражается и в выборе, потому что это один и тот же объект, а не две копии.
- Копия объекта (две истины) — selectedItem хранит копию объекта из списка. Меняется цена в списке - копия устаревает. Приходится синхронизировать вручную, и про это легко забыть.
- Ссылка по id (одна истина) — Хранится только selectedId. Сам объект всегда берётся из списка по этому id. Правка в списке сразу видна везде, синхронизировать нечего.
Принцип не зависит от библиотеки. Он одинаково применим к Redux-стору, Zustand-стору, контексту или просто объекту состояния: для каждого куска данных определяется одно место хранения, и копий этого куска больше нигде нет.
Почему выбранный элемент лучше хранить как selectedId, а не как копию объекта selectedItem?
Хранить или выводить
Перед добавлением нового поля в состояние полезно задать один вопрос: можно ли это вывести из того, что уже хранится? Если да - выводить, а не хранить. Хранить стоит только то, что нельзя восстановить из остального: введённые пользователем данные, ответы сервера, собственно факты. Всё производное (длина, сумма, отфильтрованный или отсортированный вид) вычисляется на лету.
| Данные | Хранить или выводить | Почему |
|---|---|---|
| Список товаров | Хранить | Это исходный факт, выводить не из чего |
| Длина списка | Выводить | items.length полностью определён списком |
| Итоговая сумма заказа | Выводить | Сумма позиций, иначе устареет при правке позиции |
| Текст в поле поиска | Хранить | Ввод пользователя, восстановить неоткуда |
| Отфильтрованный список | Выводить | Функция от списка и текста поиска |
| Признак пустой корзины | Выводить | items.length === 0, отдельное поле лишнее |
Простое правило: хранят входы, выводят результаты. Список и текст поиска это входы - их хранят. Отфильтрованный список это результат - его вычисляют. Чем меньше хранимых полей, тем меньше инвариантов поддерживать вручную и тем меньше поверхность для багов. Детали вычисления производных значений и их кэширование - тема следующего урока.
Эвристика: если поле всегда обновляется сразу после другого поля, скорее всего это производное значение, и его лучше не хранить, а выводить. Связка два поля меняем вместе - частый признак скрытого дубля.
В состоянии есть список позиций заказа. Нужно показывать итоговую сумму. Как с ней поступить?
Связь с другими темами
Единый источник истины это принцип, на котором стоят следующие техники:
- Производное состояние — Прямое продолжение: всё, что можно вывести, не хранят, а вычисляют из канонического источника
- Иммутабельность и нормализация — Нормализация хранит каждую сущность ровно в одном месте - тот же принцип на уровне структуры данных
Итог
- Дублирование данных это корень рассинхрона: две копии рано или поздно разойдутся
- Единый источник истины - у каждого куска состояния ровно одно каноническое место хранения
- Если значение можно вывести из другого состояния (длина, сумма, флаг пустоты), его не хранят, а вычисляют
- Хранят только то, что нельзя восстановить из остального: вводимые данные и собственно факты
- Выбранный элемент держат как id (ссылку), а не как копию объекта, чтобы правка оригинала отражалась везде
- Меньше хранимых полей - меньше мест для рассинхрона и меньше кода ручной синхронизации
Связанные уроки
- sm-02-state-taxonomy — Категория состояния подсказывает, где находится каноническое место для каждого куска данных
- sm-04-derived-state — Решение хранить или выводить раскрывается в следующем уроке про производное состояние
- sm-05-immutability-normalization — Нормализация это техника поддержания единственного канонического экземпляра каждой сущности