open atlas
↑ К треку
Архитектура фронтенда FE · 02 · 04

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

Как TanStack Query и SWR реализуют общие кэши с stale-while-revalidate семантикой, optimistic updates и стратегии инвалидации кэша для post-mount интерактивного fetching.

FE Middle ◷ 12 min
Уровень
ОсновыJuniorMiddleSenior

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

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

Если ты управлял состоянием fetch’а через useState + useEffect, ты знаешь, сколько кода нужно для загрузки, ошибок, кэширования и дедупликации. Этот урок покажет, почему библиотека справляется со всеми четырьмя задачами корректно там, где самописный код обычно ломается хотя бы на одной.

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 запросов которые на неё ссылаются. Вместе эти два вызова означают, что пользователь видит мутацию мгновенно — и на детальной странице, и в списке, без лишнего round-trip’а.

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

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 обрабатывает прямые записи кэша сущностей когда ответ мутации доступен. Теперь, когда optimistic Like откатывается через секунду, — ты знаешь куда смотреть первым: snapshot в onMutate неполный.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 5 завершено
Связанные уроки
встречается в204

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

Примени это

Примени этот урок в реальном проекте.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.