Клиентское и серверное состояние
Что это такое
Клиентским состоянием приложение владеет: какая вкладка открыта, несохранённый черновик, выбранный пользователем фильтр. Серверное состояние приложение одолжило: список заказов, профиль пользователя, остаток на складе. В коде они похожи — и то и другое вы читаете и отображаете, — однако задачи это разные, ведь сервер может изменить значение, не сообщив вам.
Почему это важно
Если обращаться с серверными данными как с обычным состоянием, получится свой набор багов: устаревший список после того, как запись изменил другой пользователь; страница, которая показывает спиннер при каждом визите, потому что ничего не сохранили; кэш, который никогда не инвалидируется. Исправление здесь не в хранилище побольше, а в признании того, что серверное состояние — кэш со сроком годности и политикой обновления. Стоит это различие сделать, и вопросы становятся конкретными — сколько это может оставаться устаревшим, что его инвалидирует, что показывать, пока оно обновляется, — а целый класс написанных вручную флагов загрузки попросту исчезает.
Ключевые идеи
Серверные данные — кэш, а не источник правды
Копию держите ради скорости, однако помните, чья она. Каждому кэшированному значению нужны три ответа: когда его загрузили, что делает его устаревшим и что делать, когда оно уже такое. Кэш без срока годности и без инвалидации — баг с длинным запалом.
// Запись кэша осмысленна только вместе с её возрастом и триггером
{ 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: так предыдущие данные остаются видимыми и во время фонового обновления.
Позвольте библиотеке делать это, если проект позволяет
Кэширование, дедупликацию, повторные попытки, фоновое обновление и инвалидацию реализуют специализированные библиотеки серверного состояния. Написать всё это вручную — отдельный проект. Модель важно знать хотя бы для того, чтобы использование такой библиотеки было решением, а не зависимостью.
Практика
Разделите состояние одного экрана на два вида
Возьмите экран, который загружает список и позволяет пользователю его фильтровать. Выпишите все значения, которые он держит, и отметьте каждое: клиентское оно или серверное.
Готово, когда: список, его состояние загрузки и его состояние ошибки — серверное состояние с одним ключом кэша, фильтр и сортировка — клиентское состояние, и ни одно значение не выполняет обе работы.
Сделайте изменение инвалидирующим точно
Добавьте в список действие удаления. Когда оно выполнится, обновите кэш, а не запрашивайте всё заново, и убедитесь, что затронутое представление верно и без полной перезагрузки.
Готово, когда: удаление строки сразу обновляет список, а последующая навигация показывает текущее состояние сервера, а не старую закэшированную версию.
Частые ошибки
- Хранение загруженных данных там же, где состояние формы — у них разные сроки жизни и правила инвалидации, и после повторной загрузки форма начинает врать; исправить: держите серверные данные в кэше с ключом по запросу, а клиентское состояние — там, где его редактируют.
- Кэш, который никогда не инвалидируется — пользователь не видит чужих записей; исправить: инвалидируйте при изменении и при фокусе окна или задайте явное время устаревания.
- Перезагрузка всего после каждой записи — страница мерцает, а сеть полна запросов, возвращающих одни и те же данные; исправить: обновляйте затронутые записи кэша.
- Спиннер при каждом обновлении в полёте — экран пустеет при каждом опросе, и пользователь теряет место; исправить: сохраняйте предыдущее значение и показывайте обновление отдельно.
- Написанные вручную флаги загрузки, ошибки и данных — комбинации противоречат друг другу; исправить: моделируйте запрос одним объединением.
Что читать дальше
- Локальное и общее состояние (02-frontend/23-state-management/01-local-versus-shared-state) — где должна жить копия, раз уж вы решили, что это копия.
- Асинхронный JavaScript (02-frontend/06-javascript/03-asynchronous-javascript) — механика запросов, отмена и обработка ошибок под ними.
- Кэширование (Redis) (05-backend-data/16-caching-redis) — то же рассуждение о кэшировании и инвалидации на сервере.