Один источник правды: состояние
На экране три позиции, в списке четыре. Ничего не сломалось — число хранилось отдельно от списка, а две копии одного факта всегда найдут способ разойтись.
Корзина показывает «3 товара» над списком из четырёх. Где-то удаление убрало строку и забыло уменьшить счётчик — или добавление обновило только сумму.
Баг не в пропущенной строке кода. Баг в том, что один и тот же факт хранится дважды. Пока это так, какой-нибудь путь через твоё приложение обновит одну копию и не обновит вторую, и никакая аккуратность этого не предотвратит: ты вручную поддерживаешь договорённость, и так вечно.
Если это можно вычислить из чего-то другого, это не состояние, а вторая копия, ждущая случая разойтись.
Состояние — это минимум
Вот определение, которое стоит унести: состояние — наименьший набор значений, из которого выводится весь экран.
Всё остальное — вычисление. Количество позиций — длина списка. Сумма — свёртка. Сообщение «ничего не найдено» — пустота отфильтрованного массива. Активна ли кнопка отправки — функция от полей. Ничто из этого состоянием не является, и хранение этого — то, с чего экраны начинают врать.
Поэтому вопрос перед добавлением чего-либо: можно ли вычислить это из того, что уже есть? Если да — вычисляй в момент отрисовки. Оно будет верным по построению, всегда, без твоего внимания.
Настоящие исключения редки и опознаваемы: значение, пересчёт которого настолько дорог, что это измеримо, или нечто, что ты намеренно хочешь заморозить на определённый момент.
Где оно живёт
Когда состав состояния известен, второе решение — где его держать: в самом нижнем компоненте, который содержит всех, кому оно нужно.
Слишком низко — не поделиться, отсюда классический ход с подъёмом состояния к общему родителю. Слишком высоко — рефлекс «положу-ка всё наверх на всякий случай» — ошибка дороже, потому что теперь несвязанные части приложения сцеплены через один объект, всё перерисовывается на каждое изменение, и никто не может сказать, какому компоненту какое поле вообще нужно.
По умолчанию держи максимально локально и поднимай только тогда, когда второму компоненту это действительно понадобилось.
Пять видов
Бо́льшая часть путаницы со состоянием рассеивается в момент, когда замечаешь: «состояние» — это пять разных вещей с разными правилами. Новички кладут все пять в одно место, обычно в глобальное хранилище, а потом удивляются, почему всё запутано.
1. Локальное состояние интерфейса. Открыта ли выпадашка, какая вкладка активна, что уже набрано в поле. Живёт внутри компонента и больше нигде. Бо́льшая часть состояния — именно эта, и бо́льшая её часть никуда не путешествует.
2. Общее клиентское состояние. Тема, вошедший пользователь, содержимое корзины. По-настоящему глобальное и по-настоящему маленькое. Вот для этого и нужен контекст или хранилище — и список должен оставаться коротким.
3. Серверное состояние. Данные, полученные из API, — и вот переосмысление, меняющее то, как люди пишут приложения: это не твоё состояние, это кэш чужого. Оно может устареть. Его можно перезапросить. Два компонента, спросившие одно и то же, не должны порождать два запроса. Ему нужны состояния загрузки и ошибки, повторы, инвалидация после изменения.
Отношение к нему как к локальному состоянию порождает самодельные флаги загрузки в каждом компоненте, дублирующиеся запросы и данные, которые остаются неверными до перезагрузки. Ровно это делает за тебя библиотека загрузки данных — кэширование, дедупликация, ревалидация, отдача устаревшего с фоновым обновлением, — и это самая ценная категория библиотек во фронтенде. Сначала пойми задачу; после этого библиотека перестаёт выглядеть лишним механизмом и начинает выглядеть очевидным ответом.
4. Состояние в адресе. Текущие фильтры, поисковый запрос, номер страницы, открытая вкладка. Это самое недоиспользуемое место во фронтенде, и проверка состоит из одного вопроса: должен ли человек иметь возможность скопировать ссылку и получить тот же самый вид? Если да — а для фильтров и поиска ответ почти всегда да, — этому место в адресе. Бесплатно получаешь возможность делиться, переживание перезагрузки и работающие кнопки «назад» и «вперёд», каждая из которых иначе становится багом, о котором сообщают пользователи.
5. Состояние формы. Черновые значения, тронутые поля, ошибки проверки. Локально по природе, разобрано в статье про формы.
Заменяй, а не мутируй
Правило, которое ты услышишь всюду, теперь с причиной: фреймворки определяют изменение сравнением ссылок. Помутируй массив на месте — ссылка та же, и с точки зрения фреймворка ничего не произошло, а экран продолжает показывать старые данные, когда данные уже новые.
Поэтому обновление производит новые значения, а не правит старые: новый массив из map или filter, новый объект через spread. Для глубоко вложенного состояния это становится неудобным, что само по себе сигнал: глубокая вложенность обычно означает неверную форму данных, и распрямить её — лучшая починка, чем более изощрённое копирование.
Эффекты и ошибка, которую делают все
Эффект существует, чтобы синхронизировать компонент с чем-то вне фреймворка: подписка, таймер, браузерное API, обработчик события, сторонний виджет.
Это весь список. Подавляюще частое злоупотребление — использовать эффект, чтобы обновлять состояние при изменении другого состояния: пересчитывать производное значение, сбрасывать поле при смене пропа, синхронизировать два куска состояния. Каждое из этого даёт лишнюю отрисовку, момент, когда на экране видно несогласованное промежуточное, и массив зависимостей, становящийся источником бесконечных циклов.
Правило: если это можно вычислить во время отрисовки, вычисляй во время отрисовки. Эффекты — для разговора с внешним миром. Если ты пишешь эффект, чья единственная работа — держать в согласии два куска твоего же состояния, значит, у тебя был один кусок состояния и вычисление, а хранение второй копии и есть настоящий баг — та же болезнь, что и счётчик в начале статьи.
И всё, что эффект устанавливает, он обязан снять: обработчик, таймер, подписку, запрос в полёте. Эта уборка — не факультативная вежливость, а разница между приложением, которым можно пользоваться час, и приложением, которым нельзя.
Сделай недопустимые состояния невозможными
Приём, который убирает баги, а не чинит их.
Три булевых значения — isLoading, hasError, isEmpty — допускают восемь комбинаций, из которых пять бессмысленны: загрузка и ошибка, ошибка и пусто. Ничто не мешает этим комбинациям возникнуть, и однажды одна возникает, порождая экран, которого никто не проектировал.
Замени их одним значением, которое может быть ровно одним из loading, error, empty или success, — и бессмыслица становится невыразимой. Отрисовка превращается в единственный выбор из четырёх случаев, и ты заметишь, что один из них никогда не обрабатывал.
С типами эта идея становится значительно сильнее, о чём следующая статья, но принять её стоит уже сейчас: меньше переменных с бо́льшим смыслом лучше, чем больше переменных с меньшим.
Перерисовка и правильный порядок волнений
Изменение состояния заново выполняет компонент, а фреймворк применяет разницу. Новички слышат «перерисовка» и представляют пересборку всей страницы; это не так, и фреймворк быстрый.
Поэтому верный порядок такой: напиши понятно, измерь, если кажется медленным, оптимизируй конкретное измеренное место. Превентивная мемоизация всего добавляет сложности, имеет собственную цену и обычно целится не в тот компонент — а читаемость обменивается на выгоду, которую никто не проверял. Как измерять, разбирает статья про производительность. До тех пор выигрывает ясность.
На практике
Выпиши состояние до написания компонента. Буквально, в комментарии или на бумаге: что этому экрану нужно знать? Потом вычеркни всё, что вычисляется. Оставшееся и есть состояние, и его обычно вдвое меньше, чем ты бы написал.
Охоться на вторые копии. Найди в своём проекте значение, хранящееся в двух местах. Удали одно и вычисли. Это самый выгодный рефакторинг во всей статье.
Перенеси одну вещь в адрес. Фильтры или поисковый запрос. Потом перезагрузи страницу и нажми «назад». Улучшение мгновенное, и пользователи его замечают.
Проверь свои эффекты. Для каждого спроси, с чем внешним он синхронизируется. Если ответ «ни с чем, он просто обновляет другое состояние» — удаляй.
Спрашивай вслух «что изменилось?», когда экран неверен. Состояние или вывод из него? Этот вопрос делит любой баг состояния надвое и быстрее чтения кода.
Объясни кому-нибудь свою модель состояния. Мысль сферы «Как учиться» действует здесь с особой силой: в голове это черновик, где ощущение связности подменяет настоящие связи, и недостающая связь всплывает во фразе, где ты пытаешься сказать, кто чем владеет.
Проверь себя
Закрой статью и ответь своими словами:
- Каково определение состояния и какая проверка вычёркивает что-либо из списка?
- Почему держать всё состояние на самом верху дерева — ошибка?
- Назови пять видов состояния и место каждого.
- Почему серверные данные — кэш, а не состояние, и что из этого следует?
- Какова проверка на то, что значению место в адресе?
- Почему мутация массива не обновляет экран?
- Для чего нужны эффекты и каково самое частое злоупотребление?
Коротко
- Две копии одного факта рано или поздно разойдутся; состояние — минимальный набор значений, из которого выводится всё остальное.
- Выводи во время отрисовки вместо хранения и держи состояние в самом нижнем компоненте, содержащем всех, кому оно нужно.
- Пять видов с разными домами: локальное интерфейсное, общее клиентское, серверный кэш, адресное и состояние формы.
- Серверные данные — кэш: они устаревают, им нужны дедупликация и ревалидация, и ради этого существуют библиотеки загрузки данных.
- Если ссылка должна воспроизводить вид, значению место в адресе: возможность делиться, перезагрузка и кнопка «назад» достаются бесплатно.
- Заменяй значения, а не мутируй, потому что изменение определяется по ссылке.
- Эффекты синхронизируют с внешним миром и обязаны убирать за собой; эффект ради согласования собственного состояния означает, что ты сохранил выводимое.
- Сверни комбинации булевых значений в один статус, чтобы недопустимые состояния стали невыразимыми, и оптимизируй перерисовку только после измерения.