Клиентское и серверное состояние — Примеры
Пример 1 — Три флага, которые могут быть истинными одновременно
Загрузку, ошибку и данные экран со списком отслеживает по отдельности. Медленная повторная попытка — и на миг экран показывает сразу спиннер, ошибку и предыдущие строки. Скажите, что допускает тип, а затем смоделируйте запрос одним значением.
let loading = false;
let error: Error | null = null;
let data: Item[] = [];
// во время неудачного обновления: loading true, ошибка задана, data всё ещё заполнена
Решение
type Query<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "error"; error: Error }
| { status: "ready"; data: T; stale: boolean };
function render(query: Query<Item[]>) {
switch (query.status) {
case "ready": return listItems(query.data, query.stale);
case "error": return showError(query.error);
case "loading": return spinner();
case "idle": return null;
}
}
Восемь комбинаций описывают три независимые переменные, и несколько из них бессмысленны: «загружается, содержит ошибку и держит данные» — не состояние, в котором может находиться запрос, однако именно это дают флаги, когда расходятся. Размеченное объединение хранит один факт, поэтому невозможные комбинации нельзя записать, а рендерер не может прочитать data вне ветви, где она существует. Флаг stale в состоянии ready — та деталь, которую стоит заметить: фоновое обновление не возвращает в loading, поэтому предыдущее значение остаётся видимым, и интерфейс говорит, что обновляется, а не пустеет до спиннера.
Пример 2 — Инвалидация перезагрузкой всего
Удаление заново запрашивает каждый список на экране, поэтому страница мерцает, а четыре запроса возвращают одни и те же данные. Скажите, что было нужно кэшу вместо этого, и обновите затронутую запись.
async function deleteOrder(id: string) {
await fetch(`/api/orders/${id}`, { method: "DELETE" });
await refetchOrders();
await refetchCustomers();
await refetchDashboard();
}
Решение
async function deleteOrder(id: string) {
const response = await fetch(`/api/orders/${id}`, { method: "DELETE" });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
cache.remove("orders:page=1", (order) => order.id === id);
cache.markStale("dashboard:summary"); // обновляется в фоне
}
После записи вопрос стоит не «перезагрузить», а «какие записи кэша затронула эта запись», ведь серверные данные — кэш, а не источник правды; назвать эти ключи и есть вся работа. Удаление строки из списка, которому она принадлежала, делает изменение мгновенно видимым без сетевого обхода. Пометка же производной сводки устаревшей позволяет ей обновиться за предыдущим значением, а не стереть его. Точная инвалидация вообще возможна благодаря ключу каждой записи по её входным данным; кэш без ключей и без срока годности — баг с длинным запалом.