State Management

TanStack Query: основы

Дашборд Vercel показывает на одном экране деплои, домены и аналитику - три блока, три запроса к API. Старый подход: на каждый блок свои useState под data, loading и error, свой useEffect с fetch, своя отмена устаревшего ответа. Если два блока спрашивают одно и то же, в сеть уходят два одинаковых запроса. TanStack Query сворачивает всё это в один вызов useQuery с ключом: дедупликация, кэш, ретраи и фоновое обновление приходят из коробки. И тот же движок работает не только в React - под Vue, Solid, Svelte и Angular это одна и та же библиотека.

  • Дашборды Vercel, Linear и десятки SaaS-панелей, где экраны построены вокруг запросов к API
  • Любая админка с таблицами, фильтрами и пагинацией: библиотека сама кэширует страницы и обновляет их в фоне
  • E-commerce: каталог, цены, наличие - серверные данные, которые устаревают и должны тихо обновляться
  • Vue-проекты на TanStack Query: тот же useQuery поверх Vue-реактивности вместо отдельной библиотеки под фреймворк
  • GitHub по докладам инженеров использует stale-while-revalidate, показывая кэш мгновенно и сверяя его в фоне

Предварительные знания

  • Разделение серверного и клиентского состояния и идея кэша с TTL
  • Загрузка данных через fetch и обработка loading/error
  • Промисы и async/await на уровне уверенного использования
  • Server-state против client-state

Как React Query стал фреймворк-независимым

Таннер Линсли выпустил React Query в 2020 году как ответ на копирование серверных данных в Redux. Идея кэша со свежестью оказалась настолько универсальной, что не имела отношения к самому React: дедупликация, staleTime, фоновая сверка - это логика работы с серверным состоянием в принципе. В 2022 году проект переименовали в TanStack Query и вынесли ядро (Query Core) во фреймворк-независимый слой, поверх которого построили адаптеры под React, Vue, Solid и Svelte. Версия 5 (конец 2023) добавила официальный адаптер под Angular и привела API к единому виду. К 2026 это де-факто стандарт работы с серверными данными в любом из этих фреймворков.

useQuery и queryKey

Ручная загрузка через useEffect требует трёх состояний, флага отмены устаревшего ответа и аккуратной обработки гонок. useQuery сворачивает это в один вызов. Запросу дают ключ (queryKey) и функцию, возвращающую промис (queryFn). Библиотека сама дедуплицирует одинаковые ключи, кэширует результат, повторяет неудачные попытки и отдаёт готовые статусы isPending, isError и data.

queryKey это не просто строка, а сериализуемый массив. Ключ ['deploys', { project: 'web' }] отличается от ['deploys', { project: 'api' }], и каждый кэшируется отдельно. Меняется ключ - меняется запрос, поэтому фильтры, id и параметры пагинации удобно класть прямо в ключ. Дедупликация работает по ключу: десять компонентов с одним queryKey дают один сетевой запрос и один общий кэш.

Фреймворк-независимость на практике: в React это useQuery из @tanstack/react-query, в Vue - useQuery из @tanstack/vue-query поверх ref. Понятия queryKey, queryFn и staleTime одни и те же, меняется только адаптер под реактивность фреймворка.

Почему queryKey задают массивом, а не просто строкой?

staleTime против gcTime

Кэш живёт по двум независимым таймерам, и их часто путают. staleTime это срок, в течение которого данные считаются свежими: пока он не истёк, повторный useQuery с тем же ключом отдаёт кэш мгновенно и в сеть не идёт. gcTime это другое - сколько неиспользуемый кэш хранится в памяти после того, как последний компонент с этим запросом размонтирован, прежде чем сборщик мусора его удалит.

ПараметрЧто задаётПо умолчанию (v5)
staleTimeКак долго данные считаются свежими и не перезапрашиваются0 (сразу устаревают)
gcTimeСколько неиспользуемый кэш живёт в памяти до удаления5 минут
refetchOnWindowFocusПерезапрос при возврате фокуса на вкладкувключено
retryСколько раз повторять неудачный запрос3

Ключевая разница: staleTime управляет тем, идти ли в сеть за свежими данными для активного запроса, а gcTime - тем, держать ли в памяти кэш запроса, на который больше никто не подписан. Пока хоть один компонент использует ключ, gcTime не отсчитывается: запрос активен. Отсчёт начинается, только когда размонтирован последний подписчик. Если за gcTime никто снова не подписался, кэш удаляется.

staleTime по умолчанию равен нулю: данные становятся устаревшими сразу после загрузки. Это безопасное значение, но при возврате фокуса или новом монтаже Query сходит за свежей копией. Для редко меняющихся данных (справочники, профиль) staleTime стоит поднять, чтобы убрать лишние фоновые запросы.

Чем staleTime отличается от gcTime?

Фоновый refetch и stale-while-revalidate

Поведение stale-while-revalidate в том и состоит: пользователю мгновенно показывают кэш, даже если он устарел, а в фоне сверяют его с сервером и при изменении подменяют. Интерфейс отзывчив - данные появляются без спиннера, потому что они уже есть в кэше - и при этом не врёт долго, потому что фоновая сверка тихо обновит снимок. Когда запускается такая сверка, видно по флагу isFetching, отдельному от isPending первой загрузки.

  1. Компонент монтируется, запрашивает ключ. Кэша нет - идёт первая загрузка, isPending равно true
  2. Данные приходят, кэшируются. Срок свежести staleTime начинает идти
  3. Срок истёк, данные стали устаревшими. При возврате фокуса или новом монтаже Query тихо идёт в сеть
  4. Во время фоновой сверки на экране всё ещё старый кэш, флаг isFetching равен true, спиннера нет
  5. Свежий ответ пришёл и подменил кэш. Все подписчики ключа перерисовались новыми данными

Фоновая сверка по умолчанию запускается по трём триггерам: возврат фокуса на вкладку (refetchOnWindowFocus), переподключение сети (refetchOnReconnect) и новый монтаж компонента (refetchOnMount), если данные устарели. Каждый из триггеров отключается отдельно, если поведение не нужно.

Данные в кэше устарели, пользователь вернулся на вкладку. Что показывает интерфейс по модели stale-while-revalidate?

Связь с другими темами

Урок про чтение данных. Дальше тема раскрывается так:

  • Server-state против client-state — Основание: Query ведёт именно серверное состояние как кэш, а не как переменную в сторе
  • TanStack Query: мутации и optimistic — Изменение данных через useMutation и инвалидация кэша после мутации
  • Server-state: паттерны — Префетч, пагинация, бесконечные и зависимые запросы поверх того же кэша

Итог

  • useQuery декларативно читает серверные данные по ключу: дедупликация, кэширование, ретраи и фоновое обновление идут из коробки вместо ручного useEffect
  • queryKey это сериализуемый массив, а не строка. Разный ключ - разный кэш, поэтому фильтры и id кладут прямо в ключ
  • staleTime задаёт, как долго снимок считается свежим и не перезапрашивается. gcTime - сколько неиспользуемый кэш живёт в памяти до сборки мусора
  • Модель stale-while-revalidate: устаревший кэш показывают мгновенно, а в фоне сверяют с сервером и при изменении подменяют
  • Ядро Query фреймворк-независимо: один и тот же движок и понятия работают под React, Vue, Solid, Svelte и Angular через адаптеры

Связанные уроки

  • sm-30-server-vs-client-state — Query это инструмент для серверного состояния, поэтому сначала нужно само разделение server-state и client-state
  • sm-32-tanstack-mutations — После чтения через useQuery идут изменения данных через useMutation с инвалидацией кэша
  • rc-36-tanstack-query — Тот же TanStack Query, разобранный внутри React-курса с тем же набором понятий
TanStack Query: основы

0

1

Войти