awesome-everything EN
↑ Обратно к восхождению

Архитектура фронтенда

Клиентский кэш: TanStack Query, SWR и stale-while-revalidate

Суть Как TanStack Query и SWR реализуют общие кэши с stale-while-revalidate семантикой, optimistic updates и стратегии инвалидации кэша для post-mount интерактивного fetching.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 12 min

Пользователь открывает страницу товара, уходит и возвращается через 10 секунд. С обычным useEffect он снова ждёт полный fetch. С TanStack Query страница мгновенно показывает кэшированный результат, пока фоновое обновление тихо проверяет наличие изменений. Те же данные, радикально разный опыт.

Паттерн stale-while-revalidate

Stale-while-revalidate — стратегия свежести кэша из RFC 5861: отдавать кэшированные данные мгновенно даже если они могут быть устаревшими, затем делать refetch в фоне. Пользователь видит контент без ожидания; UI обновляется когда приходят свежие данные.

Оба TanStack Query и SWR реализуют это:

// TanStack Query
const { data, isLoading } = useQuery({
  queryKey: ['product', id],
  queryFn: () => fetchProduct(id),
  staleTime: 30_000,   // данные "свежие" 30s — фоновый refetch не делается
  gcTime: 300_000,     // неиспользуемые записи кэша живут 5 мин до GC
});

Первый визит: isLoading=true, fetch запускается, данные приходят, isLoading=false.
Второй визит (в пределах gcTime): data возвращается мгновенно из кэша. Фоновый fetch обновляет если staleTime истёк.

queryKey — это ключ кэша. ['product', id] — отдельная запись кэша от ['product', otherId].

Single-flight дедупликация

Когда несколько компонентов монтируются одновременно и все вызывают useQuery(['products']), TanStack Query делает только один сетевой запрос. Все компоненты разделяют результат. Это называется single-flight дедупликация — она сворачивает N одинаковых in-flight запросов в один, избегая как лишнего bandwidth, так и race conditions кэша.

// 10 компонентов <ProductCard> на странице, каждый вызывает:
useQuery({ queryKey: ['products'], queryFn: fetchProducts });
// → только ОДИН сетевой запрос

SWR делает то же самое. Это делает library-based кэширование радикально безопаснее, чем собственный useState + useEffect в каждом компоненте.

Optimistic updates

Для действий которые должны ощущаться мгновенными — Like, Bookmark, Отметить как прочитанное — применить изменение локально до подтверждения сервером, затем откатить при ошибке.

const mutation = useMutation({
  mutationFn: (postId: string) => likePost(postId),
  onMutate: async (postId) => {
    // 1. Отменить любые исходящие refetch'и
    await queryClient.cancelQueries({ queryKey: ['post', postId] });
    // 2. Snapshot текущего значения
    const previous = queryClient.getQueryData(['post', postId]);
    // 3. Optimistically обновить
    queryClient.setQueryData(['post', postId], (old) => ({ ...old, liked: true, likeCount: old.likeCount + 1 }));
    return { previous };
  },
  onError: (err, postId, context) => {
    // Откатить к snapshot'у при ошибке
    queryClient.setQueryData(['post', postId], context.previous);
  },
  onSettled: (postId) => {
    // Инвалидировать для получения канонического состояния сервера
    queryClient.invalidateQueries({ queryKey: ['post', postId] });
  },
});

Паттерн: применить ожидаемый ответ сервера локально, позволить реальному ответу заменить его. Если формы совпадают — замена no-op, пользователь не видит мерцания.

Жизненный цикл optimistic update
T=0
Пользователь кликает Like → onMutate: snapshot + setQueryData(liked:true) → UI обновляется мгновенно
T=200ms
Сервер отвечает OK → onSettled: инвалидировать → фоновый refetch подтверждает
При ошибке
Сервер возвращает 429 → onError: setQueryData(snapshot) → UI откатывается

Стратегии инвалидации кэша

Когда мутация завершается успешно, устаревшие запросы должны обновиться. Три подхода:

invalidateQueries — пометить запросы устаревшими, запустить refetch при следующем observer’е. Самый безопасный; гарантирует канонические данные сервера.

queryClient.invalidateQueries({ queryKey: ['posts', userId] });

setQueryData — записать ответ мутации прямо в кэш. Самый дешёвый; лишних сетевых вызовов нет. Верно только когда ответ мутации включает полное новое состояние.

queryClient.setQueryData(['post', id], serverResponse);

refetchQueries — принудительный немедленный fetch. Используется когда нужны свежие данные прямо сейчас и нельзя доверять ответу мутации.

Большинство production кода использует оба: setQueryData для непосредственно изменённой сущности (если API её возвращает), invalidateQueries для listing запросов которые на неё ссылаются.

Почему это работает

TanStack Query v5 переименовал cacheTime в gcTime чтобы прояснить что он контролирует — не как долго данные кэшируются (это staleTime), а как долго неиспользуемые записи живут до garbage collection. Если staleTime равен 0, а gcTime — 300s, запись кэша живёт 5 минут, но при каждом обращении делается refetch. Если staleTime — 60s, refetch не делается 60 секунд после последнего успешного fetch’а.

TanStack Query vs SWR
TanStack Query v5 размер
~16 KB gzipped
SWR размер
~5 KB gzipped
Default staleTime (оба)
0ms (всегда устаревшие)
Default gcTime (TanStack Query)
5 минут
RFC 5861 (stale-while-revalidate)
HTTP аналог
Викторина

useQuery возвращает isLoading=false и data. Пользователь уходит со страницы и возвращается. Каков дефолтный UX?

Викторина

Почему TanStack Query реализует single-flight дедупликацию?

Викторина

Optimistic Like update работает, но like откатывается через мгновение. Наиболее вероятная причина?

Вспомните перед уходом
  1. 01
    Что контролирует staleTime в TanStack Query?
  2. 02
    Каков канонический паттерн optimistic update в TanStack Query?
  3. 03
    Когда использовать invalidateQueries vs setQueryData после мутации?
Итог

TanStack Query и SWR ведут общий кэш по массивам queryKey с stale-while-revalidate семантикой: staleTime контролирует свежесть (по умолчанию 0), gcTime контролирует время жизни записи (по умолчанию 5 мин). Несколько компонентов вызывающих один query key получают один сетевой запрос через single-flight дедупликацию. Optimistic updates применяют ожидаемый результат мутации локально через setQueryData со snapshot для отката; snapshot в onMutate должен повторять полную форму ответа сервера или UI мерцает когда приходят реальные данные. После мутаций invalidateQueries обеспечивает актуальность listing кэшей; setQueryData обрабатывает прямые записи кэша сущностей когда ответ мутации доступен.

Связанные уроки
встречается в178
Продолжить восхождение ↑LCP, prefetch и race conditions в интерактивном fetching
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.