EverProduct
Фронтенд

Этап 04 · Приложение

Один источник правды: состояние

На экране три позиции, в списке четыре. Ничего не сломалось — число хранилось отдельно от списка, а две копии одного факта всегда найдут способ разойтись.

Корзина показывает «3 товара» над списком из четырёх. Где-то удаление убрало строку и забыло уменьшить счётчик — или добавление обновило только сумму.

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

Если это можно вычислить из чего-то другого, это не состояние, а вторая копия, ждущая случая разойтись.

Состояние — это минимум

Вот определение, которое стоит унести: состояние — наименьший набор значений, из которого выводится весь экран.

Всё остальное — вычисление. Количество позиций — длина списка. Сумма — свёртка. Сообщение «ничего не найдено» — пустота отфильтрованного массива. Активна ли кнопка отправки — функция от полей. Ничто из этого состоянием не является, и хранение этого — то, с чего экраны начинают врать.

Поэтому вопрос перед добавлением чего-либо: можно ли вычислить это из того, что уже есть? Если да — вычисляй в момент отрисовки. Оно будет верным по построению, всегда, без твоего внимания.

Настоящие исключения редки и опознаваемы: значение, пересчёт которого настолько дорог, что это измеримо, или нечто, что ты намеренно хочешь заморозить на определённый момент.

Где оно живёт

Когда состав состояния известен, второе решение — где его держать: в самом нижнем компоненте, который содержит всех, кому оно нужно.

Слишком низко — не поделиться, отсюда классический ход с подъёмом состояния к общему родителю. Слишком высоко — рефлекс «положу-ка всё наверх на всякий случай» — ошибка дороже, потому что теперь несвязанные части приложения сцеплены через один объект, всё перерисовывается на каждое изменение, и никто не может сказать, какому компоненту какое поле вообще нужно.

По умолчанию держи максимально локально и поднимай только тогда, когда второму компоненту это действительно понадобилось.

Пять видов

Бо́льшая часть путаницы со состоянием рассеивается в момент, когда замечаешь: «состояние» — это пять разных вещей с разными правилами. Новички кладут все пять в одно место, обычно в глобальное хранилище, а потом удивляются, почему всё запутано.

1. Локальное состояние интерфейса. Открыта ли выпадашка, какая вкладка активна, что уже набрано в поле. Живёт внутри компонента и больше нигде. Бо́льшая часть состояния — именно эта, и бо́льшая её часть никуда не путешествует.

2. Общее клиентское состояние. Тема, вошедший пользователь, содержимое корзины. По-настоящему глобальное и по-настоящему маленькое. Вот для этого и нужен контекст или хранилище — и список должен оставаться коротким.

3. Серверное состояние. Данные, полученные из API, — и вот переосмысление, меняющее то, как люди пишут приложения: это не твоё состояние, это кэш чужого. Оно может устареть. Его можно перезапросить. Два компонента, спросившие одно и то же, не должны порождать два запроса. Ему нужны состояния загрузки и ошибки, повторы, инвалидация после изменения.

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

4. Состояние в адресе. Текущие фильтры, поисковый запрос, номер страницы, открытая вкладка. Это самое недоиспользуемое место во фронтенде, и проверка состоит из одного вопроса: должен ли человек иметь возможность скопировать ссылку и получить тот же самый вид? Если да — а для фильтров и поиска ответ почти всегда да, — этому место в адресе. Бесплатно получаешь возможность делиться, переживание перезагрузки и работающие кнопки «назад» и «вперёд», каждая из которых иначе становится багом, о котором сообщают пользователи.

5. Состояние формы. Черновые значения, тронутые поля, ошибки проверки. Локально по природе, разобрано в статье про формы.

Заменяй, а не мутируй

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

Поэтому обновление производит новые значения, а не правит старые: новый массив из map или filter, новый объект через spread. Для глубоко вложенного состояния это становится неудобным, что само по себе сигнал: глубокая вложенность обычно означает неверную форму данных, и распрямить её — лучшая починка, чем более изощрённое копирование.

Эффекты и ошибка, которую делают все

Эффект существует, чтобы синхронизировать компонент с чем-то вне фреймворка: подписка, таймер, браузерное API, обработчик события, сторонний виджет.

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

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

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

Сделай недопустимые состояния невозможными

Приём, который убирает баги, а не чинит их.

Три булевых значения — isLoading, hasError, isEmpty — допускают восемь комбинаций, из которых пять бессмысленны: загрузка и ошибка, ошибка и пусто. Ничто не мешает этим комбинациям возникнуть, и однажды одна возникает, порождая экран, которого никто не проектировал.

Замени их одним значением, которое может быть ровно одним из loading, error, empty или success, — и бессмыслица становится невыразимой. Отрисовка превращается в единственный выбор из четырёх случаев, и ты заметишь, что один из них никогда не обрабатывал.

С типами эта идея становится значительно сильнее, о чём следующая статья, но принять её стоит уже сейчас: меньше переменных с бо́льшим смыслом лучше, чем больше переменных с меньшим.

Перерисовка и правильный порядок волнений

Изменение состояния заново выполняет компонент, а фреймворк применяет разницу. Новички слышат «перерисовка» и представляют пересборку всей страницы; это не так, и фреймворк быстрый.

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

На практике

Выпиши состояние до написания компонента. Буквально, в комментарии или на бумаге: что этому экрану нужно знать? Потом вычеркни всё, что вычисляется. Оставшееся и есть состояние, и его обычно вдвое меньше, чем ты бы написал.

Охоться на вторые копии. Найди в своём проекте значение, хранящееся в двух местах. Удали одно и вычисли. Это самый выгодный рефакторинг во всей статье.

Перенеси одну вещь в адрес. Фильтры или поисковый запрос. Потом перезагрузи страницу и нажми «назад». Улучшение мгновенное, и пользователи его замечают.

Проверь свои эффекты. Для каждого спроси, с чем внешним он синхронизируется. Если ответ «ни с чем, он просто обновляет другое состояние» — удаляй.

Спрашивай вслух «что изменилось?», когда экран неверен. Состояние или вывод из него? Этот вопрос делит любой баг состояния надвое и быстрее чтения кода.

Объясни кому-нибудь свою модель состояния. Мысль сферы «Как учиться» действует здесь с особой силой: в голове это черновик, где ощущение связности подменяет настоящие связи, и недостающая связь всплывает во фразе, где ты пытаешься сказать, кто чем владеет.

Проверь себя

Закрой статью и ответь своими словами:

  1. Каково определение состояния и какая проверка вычёркивает что-либо из списка?
  2. Почему держать всё состояние на самом верху дерева — ошибка?
  3. Назови пять видов состояния и место каждого.
  4. Почему серверные данные — кэш, а не состояние, и что из этого следует?
  5. Какова проверка на то, что значению место в адресе?
  6. Почему мутация массива не обновляет экран?
  7. Для чего нужны эффекты и каково самое частое злоупотребление?

Коротко

  • Две копии одного факта рано или поздно разойдутся; состояние — минимальный набор значений, из которого выводится всё остальное.
  • Выводи во время отрисовки вместо хранения и держи состояние в самом нижнем компоненте, содержащем всех, кому оно нужно.
  • Пять видов с разными домами: локальное интерфейсное, общее клиентское, серверный кэш, адресное и состояние формы.
  • Серверные данные — кэш: они устаревают, им нужны дедупликация и ревалидация, и ради этого существуют библиотеки загрузки данных.
  • Если ссылка должна воспроизводить вид, значению место в адресе: возможность делиться, перезагрузка и кнопка «назад» достаются бесплатно.
  • Заменяй значения, а не мутируй, потому что изменение определяется по ссылке.
  • Эффекты синхронизируют с внешним миром и обязаны убирать за собой; эффект ради согласования собственного состояния означает, что ты сохранил выводимое.
  • Сверни комбинации булевых значений в один статус, чтобы недопустимые состояния стали невыразимыми, и оптимизируй перерисовку только после измерения.