Кастомные query и mutation кеши

TanStack Query использует несколько уровней абстракции для хранения и управления состоянием запросов:

  • QueryClient
  • QueryCache
  • MutationCache
  • Query
  • Mutation
  • Observer

По умолчанию при создании QueryClient автоматически создаются стандартные экземпляры QueryCache и MutationCache. Однако библиотека позволяет полностью переопределять их поведение.

Базовая схема выглядит так:

import {
  QueryClient,
  QueryCache,
  MutationCache,
} from '@tanstack/react-query'

const queryClient = new QueryClient({
  queryCache: new QueryCache(),
  mutationCache: new MutationCache(),
})

Кастомные кеши применяются в случаях, когда требуется:

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

Устройство QueryCache

QueryCache представляет собой контейнер всех query-объектов приложения.

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

  • query key;
  • статус;
  • данные;
  • ошибку;
  • observers;
  • timestamps;
  • metadata.

Стандартное создание кеша:

const queryCache = new QueryCache()

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

const queryClient = new QueryClient({
  queryCache,
})

Глобальные callbacks QueryCache

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

  • onError
  • onSuccess
  • onSettled

Пример:

const queryCache = new QueryCache({
  onError: (error, query) => {
    console.error('Query error:', error)
    console.log('Query key:', query.queryKey)
  },

  onSuccess: (data, query) => {
    console.log('Query success:', query.queryKey)
  },

  onSettled: (data, error, query) => {
    console.log('Query finished')
  },
})

Эти callbacks вызываются для всех запросов в приложении.


Централизованная обработка ошибок

Одна из самых распространённых задач кастомного кеша — единая обработка ошибок API.

Пример:

const queryCache = new QueryCache({
  onError: (error) => {
    if (error.response?.status === 401) {
      logout()
    }

    if (error.response?.status >= 500) {
      showGlobalErrorNotification()
    }
  },
})

Преимущества такого подхода:

  • отсутствие дублирования;
  • единая точка обработки;
  • консистентное поведение приложения;
  • упрощение query hooks.

Интеграция с логированием

Кастомный кеш удобно использовать для интеграции с системами логирования.

Пример отправки ошибок в monitoring:

const queryCache = new QueryCache({
  onError: (error, query) => {
    monitoring.captureException(error, {
      queryKey: query.queryKey,
      meta: query.meta,
    })
  },
})

Часто используются:

  • Sentry;
  • Datadog;
  • New Relic;
  • OpenTelemetry;
  • Grafana.

Использование query.meta

Каждый query может содержать meta.

Пример:

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

  meta: {
    feature: 'users-page',
    critical: true,
  },
})

Доступ в QueryCache:

const queryCache = new QueryCache({
  onError: (error, query) => {
    console.log(query.meta)
  },
})

meta особенно полезен для:

  • аналитики;
  • feature tagging;
  • трассировки;
  • приоритизации;
  • условной обработки ошибок.

Подписка на события QueryCache

Кеш поддерживает подписку на изменения.

Пример:

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

Типы событий:

  • added
  • removed
  • updated

Структура события:

{
  type: 'updated',
  query,
  action,
}

Отслеживание обновлений запросов

Пример анализа активности запросов:

queryCache.subscribe((event) => {
  if (event.type === 'updated') {
    console.log('Updated query:', event.query.queryKey)
  }
})

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

  • строить devtools;
  • анализировать производительность;
  • отслеживать частоту refetch;
  • реализовывать telemetry.

Получение query из кеша

Поиск query:

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

Получение нескольких запросов:

const queries = queryCache.findAll({
  queryKey: ['users'],
})

Работа с Query объектом

Объект query содержит большое количество внутренней информации.

Пример:

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

console.log(query.state)

Структура state:

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

Ручное удаление query

Удаление query:

queryCache.remove(query)

Полная очистка:

queryCache.clear()

Кастомная очистка кеша

Иногда требуется нестандартная стратегия удаления.

Пример:

queryCache.findAll().forEach((query) => {
  const isOld =
    Date.now() - query.state.dataUpdatedAt > 60000

  if (isOld) {
    queryCache.remove(query)
  }
})

Реализация soft eviction

Soft eviction — удаление редко используемых данных.

Пример:

queryCache.findAll().forEach((query) => {
  const observersCount = query.getObserversCount()

  if (observersCount === 0) {
    queryCache.remove(query)
  }
})

MutationCache

MutationCache работает аналогично QueryCache, но хранит мутации.

Создание:

const mutationCache = new MutationCache()

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

const queryClient = new QueryClient({
  mutationCache,
})

Глобальные callbacks MutationCache

Поддерживаются:

  • onError
  • onSuccess
  • onSettled
  • onMutate

Пример:

const mutationCache = new MutationCache({
  onSuccess: (data, variables, context, mutation) => {
    console.log('Mutation success')
  },

  onError: (error, variables, context, mutation) => {
    console.error(error)
  },
})

Глобальная обработка mutation ошибок

Пример:

const mutationCache = new MutationCache({
  onError: (error) => {
    toast.error(error.message)
  },
})

Такой подход устраняет повторение одинакового кода в каждой мутации.


Логирование мутаций

Пример:

const mutationCache = new MutationCache({
  onSuccess: (data, variables, context, mutation) => {
    analytics.track('mutation_success', {
      mutationKey: mutation.options.mutationKey,
    })
  },
})

Mutation events

Подписка:

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

Типы событий:

  • added
  • removed
  • updated

Анализ mutation состояния

Пример:

mutationCache.subscribe((event) => {
  if (event.type === 'updated') {
    console.log(event.mutation.state.status)
  }
})

Возможные статусы:

  • idle
  • pending
  • success
  • error

Mutation meta

Мутации также поддерживают meta.

Пример:

useMutation({
  mutationFn: saveUser,

  meta: {
    audit: true,
    feature: 'profile',
  },
})

Использование:

const mutationCache = new MutationCache({
  onSuccess: (data, variables, context, mutation) => {
    console.log(mutation.meta)
  },
})

Кастомный audit trail

Пример ведения аудита:

const mutationCache = new MutationCache({
  onSuccess: (data, variables, context, mutation) => {
    auditService.log({
      action: mutation.options.mutationKey,
      variables,
      timestamp: Date.now(),
    })
  },
})

Глобальная invalidation логика

Кастомный кеш позволяет централизовать invalidation.

Пример:

const mutationCache = new MutationCache({
  onSuccess: (data, variables, context, mutation) => {
    if (mutation.options.mutationKey?.includes('user')) {
      queryClient.invalidateQueries({
        queryKey: ['users'],
      })
    }
  },
})

Автоматическая синхронизация сущностей

Пример обновления сущностей после mutation:

const mutationCache = new MutationCache({
  onSuccess: (data) => {
    queryClient.setQueryData(
      ['user', data.id],
      data
    )
  },
})

Создание собственного класса QueryCache

TanStack Query позволяет наследоваться от кешей.

Пример:

class CustomQueryCache extends QueryCache {
  notify(event) {
    console.log('Custom notify:', event)

    super.notify(event)
  }
}

Использование:

const queryClient = new QueryClient({
  queryCache: new CustomQueryCache(),
})

Переопределение notify

Метод notify отвечает за распространение событий подписчикам.

Пример:

class DebugQueryCache extends QueryCache {
  notify(event) {
    performance.mark('query-notify')

    super.notify(event)
  }
}

Создание собственного MutationCache

Пример:

class CustomMutationCache extends MutationCache {
  notify(event) {
    console.log('Mutation event:', event)

    super.notify(event)
  }
}

Инструментирование производительности

Кастомные кеши подходят для performance profiling.

Пример:

class ProfilingQueryCache extends QueryCache {
  notify(event) {
    const start = performance.now()

    super.notify(event)

    const end = performance.now()

    console.log('Notify duration:', end - start)
  }
}

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

Пример:

const queryCache = new QueryCache({
  onSuccess: (data, query) => {
    telemetry.addSpan({
      name: 'query_success',
      attributes: {
        queryKey: JSON.stringify(query.queryKey),
      },
    })
  },
})

Отслеживание медленных запросов

Пример:

const queryCache = new QueryCache({
  onSettled: (data, error, query) => {
    const duration =
      Date.now() - query.state.fetchMeta.startedAt

    if (duration > 3000) {
      console.warn('Slow query:', query.queryKey)
    }
  },
})

Кастомный мониторинг retry

Пример:

queryCache.subscribe((event) => {
  if (event.type === 'updated') {
    const retries = event.query.state.fetchFailureCount

    if (retries > 2) {
      console.warn('Too many retries')
    }
  }
})

Реализация глобального rate limiting

Пример ограничения запросов:

class RateLimitedQueryCache extends QueryCache {
  activeRequests = 0

  notify(event) {
    if (event.type === 'updated') {
      const fetching =
        event.query.state.fetchStatus === 'fetching'

      if (fetching) {
        this.activeRequests++
      }
    }

    super.notify(event)
  }
}

Связь QueryCache и QueryClient

QueryClient является фасадом поверх кешей.

Схема:

QueryClient
 ├── QueryCache
 └── MutationCache

Все операции:

  • invalidation;
  • refetch;
  • setQueryData;
  • cancelQueries;

в конечном итоге работают через внутренние кеши.


Доступ к кешу через QueryClient

Получение QueryCache:

const queryCache = queryClient.getQueryCache()

Получение MutationCache:

const mutationCache =
  queryClient.getMutationCache()

Низкоуровневое управление кешем

Пример:

const queryCache = queryClient.getQueryCache()

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

console.log(query.state.data)

Сценарии использования кастомных кешей

Наиболее распространённые кейсы:

Сценарий Назначение
Monitoring Отслеживание ошибок
Analytics Сбор метрик
Audit logs Журналирование действий
Performance profiling Анализ производительности
Error normalization Унификация ошибок
Telemetry Трассировка событий
Cache diagnostics Диагностика кеша
Smart eviction Умная очистка
Debugging Отладка
Multi-tab sync Синхронизация

Ограничения кастомных кешей

Неправильная реализация может привести к:

  • утечкам памяти;
  • лишним re-render;
  • деградации производительности;
  • циклическим обновлениям;
  • дублирующимся событиям;
  • неконсистентному состоянию.

Особенно опасны:

  • тяжёлые вычисления внутри notify;
  • синхронные сетевые операции;
  • мутация внутренних state объектов;
  • рекурсивные invalidate операции.

Практика безопасной реализации

Рекомендуемые подходы:

  • минимальная логика внутри callbacks;
  • асинхронное логирование;
  • отсутствие side effects в notify;
  • immutable-подход;
  • throttling аналитики;
  • debounce событий;
  • централизованная сериализация ошибок.

Архитектура enterprise-приложений

В крупных приложениях кастомные кеши часто становятся инфраструктурным слоем.

Типичная схема:

TanStack Query
    ↓
Custom QueryCache
    ↓
Telemetry Layer
    ↓
Monitoring
    ↓
Analytics

Такая архитектура позволяет:

  • централизовать observability;
  • уменьшить связанность UI и инфраструктуры;
  • стандартизировать обработку ошибок;
  • реализовать unified tracing;
  • упростить диагностику production-проблем.