Клиентское и серверное состояние

Что это такое

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

Почему это важно

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

Ключевые идеи

Серверные данные — кэш, а не источник правды

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

// Запись кэша осмысленна только вместе с её возрастом и триггером
{ data, fetchedAt: Date.now(), key: "orders:page=1" }

Ключуйте каждый запрос по его входным данным

Два запроса к одному ресурсу с разными параметрами попадут в разные записи кэша. Ключ по входным данным — запросу, странице, фильтру — позволяет ещё до загрузки ответить «у нас это уже есть?» и инвалидировать точно, а не всё подряд.

Моделируйте запрос, а не три флага

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

type Query<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "error"; error: Error }
  | { status: "ready"; data: T; stale: boolean };

Инвалидируйте после изменения — осознанно

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

Показывайте старое значение, пока идёт обновление

Список, который схлопывается в спиннер при каждом обновлении, хуже списка, который остаётся на месте с неприметной пометкой «обновляется». Именно поэтому состояние ready несёт флаг stale, а не возвращается в loading: так предыдущие данные остаются видимыми и во время фонового обновления.

Позвольте библиотеке делать это, если проект позволяет

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

Практика

Разделите состояние одного экрана на два вида

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

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

Сделайте изменение инвалидирующим точно

Добавьте в список действие удаления. Когда оно выполнится, обновите кэш, а не запрашивайте всё заново, и убедитесь, что затронутое представление верно и без полной перезагрузки.

Готово, когда: удаление строки сразу обновляет список, а последующая навигация показывает текущее состояние сервера, а не старую закэшированную версию.

Частые ошибки

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

Что читать дальше