QueryCache и MutationCache

В основе работы TanStack Query лежат два глобальных хранилища:

  • QueryCache — кэш запросов
  • MutationCache — кэш мутаций

Они являются частью экземпляра QueryClient и отвечают за централизованное хранение состояния данных, подписок, статусов, ошибок, таймеров и жизненного цикла запросов.

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

QueryClient
 ├── QueryCache
 │    ├── Query
 │    ├── Query
 │    └── Query
 │
 └── MutationCache
      ├── Mutation
      ├── Mutation
      └── Mutation

Каждый вызов useQuery() создает или использует объект Query.

Каждый вызов useMutation() создает объект Mutation.


QueryCache

Назначение QueryCache

QueryCache хранит все query-запросы приложения.

Именно здесь находятся:

  • кэшированные данные
  • статусы загрузки
  • ошибки
  • время последнего обновления
  • подписчики
  • таймеры garbage collection
  • информация об invalidation
  • состояние stale/fresh

Пример:

const queryClient = new QueryClient()

console.log(queryClient.getQueryCache())

Как QueryCache хранит данные

Каждый query внутри кэша содержит:

{
  queryKey,
  queryHash,
  state,
  observers,
  gcTimeout,
  options
}

queryKey

Оригинальный ключ запроса:

['posts', 5]

queryHash

Внутренний сериализованный hash:

'["posts",5]'

Именно по hash TanStack Query определяет уникальность запроса.


state

Содержит текущее состояние query.

Пример структуры:

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

observers

Массив подписчиков.

Обычно это компоненты React, использующие useQuery().


Жизненный цикл Query внутри QueryCache

Создание query

При первом вызове:

useQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts
})

TanStack Query:

  1. Проверяет наличие query в QueryCache

  2. Если query отсутствует:

    • создается новый объект Query
    • добавляется в QueryCache
  3. Запускается fetch


Повторное использование query

Если другой компонент использует:

useQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts
})

новый запрос не создается.

Оба компонента подписываются на один Query.


Структура Query

Внутренне Query является state machine.

Упрощенная схема:

idle
  ↓
pending
  ↓
success
  ↓
stale
  ↓
refetching

Либо:

pending
  ↓
error

Доступ к QueryCache

getQueryCache()

const queryCache = queryClient.getQueryCache()

find()

Поиск query:

const query = queryCache.find({
  queryKey: ['posts']
})

getAll()

Получение всех query:

const queries = queryCache.getAll()

Подписка на QueryCache

subscribe()

QueryCache поддерживает глобальные подписки.

Пример:

const unsubscribe = queryCache.subscribe((event) => {
  console.log(event)
})

События QueryCache

queryAdded

Срабатывает при добавлении query.

{
  type: 'added',
  query
}

queryRemoved

Срабатывает при удалении query.

{
  type: 'removed',
  query
}

queryUpdated

Срабатывает при обновлении query.

{
  type: 'updated',
  query,
  action
}

Практическое применение subscribe()

Логирование

queryCache.subscribe((event) => {
  console.log(
    event.type,
    event.query.queryKey
  )
})

Интеграция с analytics

queryCache.subscribe((event) => {
  if (event.type === 'updated') {
    analytics.track('query_updated')
  }
})

Глобальная отладка

queryCache.subscribe((event) => {
  console.log(event.query.state)
})

Garbage Collection в QueryCache

Почему query удаляются

TanStack Query не хранит query бесконечно.

Если query:

  • не используется
  • не имеет observers
  • превысил gcTime

то он удаляется из QueryCache.


Пример

useQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts,
  gcTime: 1000 * 60 * 5
})

Через 5 минут после потери подписчиков query будет удален.


Активные и неактивные query

Active query

Есть хотя бы один observer.

Component A
   ↓
useQuery()
   ↓
Query observer exists

Inactive query

Подписчиков больше нет.

No components
   ↓
No observers
   ↓
Inactive query

После этого запускается countdown garbage collection.


staleTime и QueryCache

staleTime влияет только на freshness query.

Он не удаляет query из кэша.

useQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts,
  staleTime: 60000
})

В течение минуты query считается fresh.


invalidateQueries и QueryCache

При invalidation QueryCache помечает query как stale.

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

Внутренне происходит:

Query.state.isInvalidated = true

removeQueries

Полное удаление query из QueryCache.

queryClient.removeQueries({
  queryKey: ['posts']
})

После удаления:

  • исчезают данные
  • удаляются observers
  • очищается state

resetQueries

Сброс query в исходное состояние.

queryClient.resetQueries({
  queryKey: ['posts']
})

Данные очищаются, но query остается в QueryCache.


clear()

Полная очистка QueryCache.

queryClient.clear()

Удаляются:

  • query
  • mutation
  • observers
  • timers

Внутреннее устройство QueryObserver

Компоненты React не работают напрямую с Query.

Между ними находится QueryObserver.

Схема:

Component
   ↓
QueryObserver
   ↓
Query
   ↓
QueryCache

Observer:

  • подписывается на Query
  • следит за изменениями
  • вызывает re-render

notifyManager и батчинг

TanStack Query использует notifyManager для оптимизации обновлений.

Вместо множества re-render:

Query update
Query update
Query update

TanStack Query делает batching:

Batch
 ├── update
 ├── update
 └── update

Это уменьшает нагрузку на React.


MutationCache

Назначение MutationCache

MutationCache хранит все мутации приложения.

Каждый useMutation() создает объект Mutation.


Что хранит MutationCache

Каждая mutation содержит:

{
  mutationId,
  state,
  options,
  observers
}

State мутации

Пример:

{
  status,
  data,
  error,
  variables,
  submittedAt
}

Статусы mutation

idle

Мутация еще не запускалась.


pending

Мутация выполняется.


success

Успешное завершение.


error

Ошибка выполнения.


Создание mutation

const mutation = useMutation({
  mutationFn: createPost
})

После вызова:

mutation.mutate(data)

создается Mutation object в MutationCache.


Временный характер MutationCache

В отличие от QueryCache, мутации обычно живут недолго.

После завершения они:

  • становятся inactive
  • могут быть удалены garbage collector
  • редко переиспользуются

Доступ к MutationCache

getMutationCache()

const mutationCache =
  queryClient.getMutationCache()

Подписка на MutationCache

mutationCache.subscribe((event) => {
  console.log(event)
})

События MutationCache

added

Создание mutation.


removed

Удаление mutation.


updated

Изменение состояния mutation.


Глобальная обработка ошибок мутаций

Одно из главных применений MutationCache — централизованная обработка ошибок.


Глобальный onError

const queryClient = new QueryClient({
  mutationCache: new MutationCache({
    onError: (error) => {
      console.error(error)
    }
  })
})

Глобальный onSuccess

const queryClient = new QueryClient({
  mutationCache: new MutationCache({
    onSuccess: (data) => {
      console.log(data)
    }
  })
})

Глобальный onSettled

const queryClient = new QueryClient({
  mutationCache: new MutationCache({
    onSettled: () => {
      console.log('finished')
    }
  })
})

QueryCache vs MutationCache

QueryCache MutationCache
Хранит query Хранит mutation
Долгоживущий Краткоживущий
Переиспользуется Обычно одноразовый
Кэширует данные Не предназначен для кэширования
Поддерживает stale/fresh Нет stale-механизма
Использует queryKey Использует mutationId

Взаимодействие QueryCache и MutationCache

Основной сценарий:

Mutation
   ↓
Server update
   ↓
invalidateQueries()
   ↓
QueryCache refresh

Типичный flow после mutation

const queryClient = useQueryClient()

const mutation = useMutation({
  mutationFn: updatePost,

  onSuccess: () => {
    queryClient.invalidateQueries({
      queryKey: ['posts']
    })
  }
})

Почему mutation не обновляет QueryCache автоматически

TanStack Query не знает:

  • какие query связаны с mutation
  • как именно изменились данные
  • какие страницы затронуты
  • какие фильтры используются

Поэтому разработчик сам управляет синхронизацией.


setQueryData и QueryCache

Mutation может обновлять QueryCache вручную.

queryClient.setQueryData(
  ['posts', id],
  (oldData) => ({
    ...oldData,
    title: 'New title'
  })
)

Внутренне:

Mutation success
   ↓
QueryCache.find()
   ↓
Query.state.data update
   ↓
Observers notified
   ↓
React re-render

Devtools и QueryCache

TanStack Query Devtools напрямую работают с QueryCache.

Они отображают:

  • query keys
  • observers
  • stale/fresh
  • cache time
  • состояние fetch
  • garbage collection

Devtools и MutationCache

Devtools также показывают:

  • mutation state
  • pending mutations
  • retries
  • ошибки
  • paused mutations

Внутренний flow Query

Упрощенная схема:

useQuery()
   ↓
QueryObserver
   ↓
QueryCache.find()
   ↓
Query exists?
   ├── yes → subscribe
   └── no
         ↓
      create Query
         ↓
      fetch()
         ↓
      update state
         ↓
      notify observers

Внутренний flow Mutation

mutate()
   ↓
create Mutation
   ↓
MutationCache.add()
   ↓
execute mutationFn
   ↓
success/error
   ↓
callbacks
   ↓
invalidateQueries()

Почему QueryCache является центральным элементом TanStack Query

Именно QueryCache обеспечивает:

  • дедупликацию запросов
  • разделение данных между компонентами
  • реактивность
  • автоматический refetch
  • stale/fresh механизм
  • garbage collection
  • background updates
  • синхронизацию состояния

Без QueryCache TanStack Query превращается в обычный fetch wrapper.


Когда требуется работа напрямую с QueryCache

Прямое взаимодействие с QueryCache используется редко, но важно в:

  • devtools
  • analytics
  • логировании
  • custom debugging
  • SSR hydration
  • offline persistence
  • сложных realtime-сценариях
  • библиотеках поверх TanStack Query

Когда требуется работа напрямую с MutationCache

MutationCache обычно используют для:

  • глобальной обработки ошибок
  • toast-уведомлений
  • централизованного retry
  • offline mutation queue
  • аналитики
  • логирования API-операций