Жизненный цикл запроса

В TanStack Query любой запрос проходит последовательность состояний и этапов обработки. Библиотека отслеживает:

  • момент создания запроса;
  • запуск сетевого обращения;
  • получение ответа;
  • обновление кэша;
  • повторные запросы;
  • устаревание данных;
  • удаление из памяти.

Жизненный цикл запроса строится вокруг объекта Query, который хранится внутри Query Cache. Компоненты через useQuery() подписываются на этот объект и автоматически получают обновления состояния.

Полный цикл выглядит следующим образом:

  1. Компонент вызывает useQuery
  2. Формируется query key
  3. TanStack Query ищет данные в кэше
  4. При отсутствии данных запускается queryFn
  5. Выполняется HTTP-запрос
  6. Данные сохраняются в кэш
  7. Компоненты получают обновление
  8. Через некоторое время данные становятся stale
  9. Может происходить повторный refetch
  10. При отсутствии подписчиков запрос становится inactive
  11. После gcTime запрос удаляется из памяти

Создание запроса

Каждый запрос начинается с вызова useQuery.

import { useQuery } from '@tanstack/react-query'

function Users() {
  const query = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers
  })

  return <div>...</div>
}

В этот момент TanStack Query:

  • создает внутренний Query Observer;
  • проверяет наличие Query в кэше;
  • подписывает компонент на изменения;
  • определяет необходимость сетевого запроса.

Ключ queryKey играет центральную роль. Именно он идентифицирует запрос.


Поиск данных в кэше

Перед выполнением сетевого запроса библиотека всегда проверяет Query Cache.

Если данные уже существуют:

queryKey: ['users']

то возможны два сценария:

Данные свежие

Если данные еще не устарели (staleTime не истек), запрос к серверу не выполняется.

Компонент сразу получает данные из памяти.

Данные устарели

Если данные stale, TanStack Query:

  • мгновенно показывает кэш;
  • параллельно запускает background refetch.

Это один из важнейших механизмов библиотеки.


Состояние pending

Во время первого выполнения запроса состояние становится pending.

const {
  status
} = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers
})

Возможные значения:

pending
success
error

При pending:

status === 'pending'

или:

isPending === true

Компонент обычно показывает loader.

if (query.isPending) {
  return <Spinner />
}

Выполнение queryFn

queryFn — функция получения данных.

async function fetchUsers() {
  const response = await fetch('/api/users')

  if (!response.ok) {
    throw new Error('Ошибка загрузки')
  }

  return response.json()
}

TanStack Query:

  • вызывает функцию;
  • ожидает Promise;
  • отслеживает ошибки;
  • сохраняет результат.

Если Promise успешно завершился — запрос получает статус success.

Если Promise выбросил исключение — статус становится error.


Переход в состояние success

После успешного получения данных:

{
  status: 'success'
}

в Query Cache сохраняются:

  • данные;
  • timestamp обновления;
  • metadata запроса;
  • информация о stale-состоянии.

Компоненты автоматически перерисовываются.

const { data } = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers
})

Кэширование результата

После завершения запроса данные остаются в памяти.

Это позволяет:

  • избегать повторных запросов;
  • мгновенно показывать интерфейс;
  • синхронизировать состояние между компонентами.

Пример:

function UsersList() {
  useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers
  })
}

function SidebarUsers() {
  useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers
  })
}

HTTP-запрос выполнится только один раз.

Оба компонента используют общий Query Cache.


Fresh и stale данные

Одно из важнейших понятий TanStack Query — свежесть данных.

По умолчанию данные считаются stale сразу после получения.

useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers
})

Эквивалентно:

staleTime: 0

Настройка staleTime

staleTime определяет время свежести данных.

useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  staleTime: 1000 * 60
})

В течение минуты:

  • refetch не выполняется автоматически;
  • данные считаются актуальными;
  • повторные монтирования используют кэш.

Поведение stale запросов

Когда данные становятся stale:

  • UI продолжает использовать кэш;
  • Query может выполнять background refetch;
  • старые данные не удаляются.

Это важное отличие от классического подхода с loader на каждое обновление.


Background Refetch

Фоновое обновление — ключевая особенность TanStack Query.

При stale данных библиотека может автоматически обновлять запрос:

  • при фокусе окна;
  • при reconnect;
  • при mount компонента;
  • по интервалу;
  • вручную.

Во время background refetch:

isFetching === true

Но:

isPending === false

То есть данные уже есть, но происходит обновление.


Разница между isPending и isFetching

isPending

Означает:

  • данных еще нет;
  • запрос выполняется впервые.

isFetching

Означает:

  • выполняется любой сетевой запрос;
  • включая background refetch.

Пример:

const query = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers
})

query.isPending
query.isFetching

Состояние error

При ошибке:

status === 'error'

в объекте запроса появляется:

error

Пример:

if (query.isError) {
  return <div>{query.error.message}</div>
}

Ошибка сохраняется внутри Query Cache.


Retry-механизм

По умолчанию TanStack Query автоматически повторяет запросы.

retry: 3

Поведение:

  1. запрос завершился ошибкой;
  2. библиотека ждет delay;
  3. выполняется повтор;
  4. процесс повторяется.

Настройка retry

useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  retry: 5
})

Полное отключение:

retry: false

Retry Delay

Задержка между попытками:

retryDelay: 1000

Либо функция:

retryDelay: attempt =>
  Math.min(1000 * 2 ** attempt, 30000)

Так реализуется exponential backoff.


Повторный mount компонента

Когда компонент размонтируется:

return null

TanStack Query не удаляет Query сразу.

Запрос становится inactive.


Inactive Query

Inactive Query:

  • остается в памяти;
  • хранит кэш;
  • не имеет активных подписчиков.

Если компонент снова смонтируется:

useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers
})

данные будут взяты из кэша.


Garbage Collection

Через некоторое время inactive Query удаляется.

Настройка:

gcTime: 1000 * 60 * 5

По умолчанию — 5 минут.

После истечения времени:

  • Query удаляется;
  • кэш очищается;
  • память освобождается.

staleTime и gcTime

Эти параметры часто путают.

staleTime

Отвечает за свежесть данных.

gcTime

Отвечает за время хранения inactive Query в памяти.


Пример жизненного цикла

useQuery({
  queryKey: ['profile'],
  queryFn: fetchProfile,
  staleTime: 60000,
  gcTime: 300000
})

Сценарий:

  1. Выполняется запрос
  2. Данные сохраняются
  3. 60 секунд данные fresh
  4. После 60 секунд — stale
  5. Компонент размонтируется
  6. Query становится inactive
  7. Через 5 минут Query удаляется

Refetch on Window Focus

По умолчанию TanStack Query обновляет stale запросы при возврате фокуса окна.

Например:

  1. пользователь переключился на другую вкладку;
  2. данные устарели;
  3. пользователь вернулся;
  4. выполняется refetch.

Настройка:

refetchOnWindowFocus: false

Refetch on Reconnect

При восстановлении интернета:

refetchOnReconnect: true

TanStack Query автоматически обновляет stale запросы.


Refetch on Mount

При повторном mount:

refetchOnMount: true

Если данные stale — запрос обновится.


Интервальный refetch

Периодическое обновление:

refetchInterval: 5000

Запрос будет выполняться каждые 5 секунд.

Даже без действий пользователя.


Отмена запросов

TanStack Query поддерживает AbortController.

useQuery({
  queryKey: ['users'],
  queryFn: async ({ signal }) => {
    const response = await fetch('/api/users', {
      signal
    })

    return response.json()
  }
})

При отмене:

  • запрос прерывается;
  • Promise отменяется;
  • предотвращаются race conditions.

Дедупликация запросов

Если несколько компонентов одновременно вызывают одинаковый запрос:

['users']

TanStack Query выполняет только один HTTP-запрос.

Остальные подписываются на тот же Promise.


Observer-система

Каждый useQuery() создает Query Observer.

Observer:

  • подписывается на Query;
  • получает обновления;
  • вызывает ререндер.

Один Query может иметь множество Observer.


Изменение состояния запроса

Внутри жизненного цикла Query может переходить между состояниями:

pending -> success
pending -> error
success -> fetching
error -> fetching

При refetch:

success + fetching

То есть старые данные продолжают существовать.


Структура Query State

Внутреннее состояние Query содержит:

{
  data,
  error,
  status,
  fetchStatus,
  dataUpdatedAt,
  errorUpdatedAt
}

fetchStatus

fetchStatus отличается от status.

Возможные значения:

fetching
paused
idle

Пример:

status: 'success'
fetchStatus: 'fetching'

Это означает:

  • данные уже есть;
  • выполняется refetch.

Пауза запросов

Если сеть недоступна:

fetchStatus === 'paused'

После восстановления соединения запрос продолжится.


Ручной refetch

Любой Query можно обновить вручную.

const { refetch } = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers
})

Вызов:

await refetch()

Инвалидация запросов

Invalidate — ключевой механизм обновления данных.

queryClient.invalidateQueries({
  queryKey: ['users']
})

После invalidation:

  • Query помечается stale;
  • может запускаться refetch.

Влияние мутаций на жизненный цикл

После mutation часто выполняется invalidation.

await mutation.mutateAsync(data)

queryClient.invalidateQueries({
  queryKey: ['users']
})

Это запускает новый цикл:

stale -> fetching -> success

Placeholder Data

Временные данные:

placeholderData: []

Используются до завершения запроса.

Но не сохраняются в кэш.


Initial Data

Начальные данные:

initialData: []

В отличие от placeholderData:

  • сохраняются в Query Cache;
  • считаются полноценными данными.

keepPreviousData

Полезно для пагинации.

useQuery({
  queryKey: ['users', page],
  queryFn: () => fetchUsers(page),
  placeholderData: keepPreviousData
})

Старые данные остаются на экране во время загрузки новой страницы.


Жизненный цикл при смене queryKey

Изменение ключа создает новый Query.

['users', 1]
['users', 2]

Это два независимых жизненных цикла.


Prefetch-запросы

Запрос можно заранее поместить в кэш.

await queryClient.prefetchQuery({
  queryKey: ['users'],
  queryFn: fetchUsers
})

При последующем mount:

  • данные уже будут доступны;
  • UI отобразится мгновенно.

Hydration и SSR

При SSR жизненный цикл меняется.

Сервер:

  1. выполняет запрос;
  2. сериализует Query Cache.

Клиент:

  1. гидратирует кэш;
  2. использует готовые данные;
  3. может запускать refetch.

Devtools и отслеживание жизненного цикла

TanStack Query Devtools позволяют видеть:

  • все Query;
  • состояния;
  • stale/fresh;
  • inactive Query;
  • refetch;
  • retry;
  • время жизни кэша.

Подключение:

import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
<ReactQueryDevtools initialIsOpen={false} />

Полная схема жизненного цикла

mount компонента
    ↓
поиск Query в кэше
    ↓
fresh? ── yes ──→ возврат кэша
    ↓ no
запуск queryFn
    ↓
pending
    ↓
success/error
    ↓
сохранение в кэш
    ↓
данные становятся stale
    ↓
background refetch
    ↓
inactive
    ↓
garbage collection