Проблемы с кешированием

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

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

Ключевые сущности, влияющие на кеш:

  • queryKey — идентификатор данных
  • staleTime — время актуальности
  • cacheTime (в новых версиях gcTime) — время жизни неиспользуемого кеша
  • queryClient — центральное хранилище состояния

Ошибки в любой из этих частей приводят к рассинхронизации UI и сервера.


Дублирование данных из-за неправильного queryKey

Одной из самых частых проблем является некорректное формирование ключей запросов. TanStack Query считает данные одинаковыми только при полном совпадении queryKey.

Проблема возникает, когда ключи формируются неструктурированно:

useQuery({
  queryKey: ['users', id],
  queryFn: () => fetchUser(id),
});

useQuery({
  queryKey: ['users', String(id)],
  queryFn: () => fetchUser(id),
});

На уровне логики это один и тот же запрос, но для кеша это два разных набора данных. В результате:

  • создаются дублирующие записи в кеше
  • запросы выполняются повторно
  • инвалидирование работает непредсказуемо

Особенно критично это при динамических фильтрах:

queryKey: ['products', { category, page }]

Если объект создаётся без стабилизации (например, новые ссылки на каждый рендер), кеш начинает фрагментироваться.


Проблема нестабильных ссылок в queryKey

TanStack Query использует глубокое сравнение ключей, но не нормализует объекты автоматически. Это приводит к ситуации, когда одинаковые по содержанию ключи считаются разными.

queryKey: ['items', { filter: { status: 'active' } }]

Если на каждом рендере создаётся новый объект filter, кеш теряет эффективность.

Типичный симптом:

  • одинаковые запросы выполняются снова и снова
  • DevTools показывает множество похожих query в состоянии loading
  • кеш не переиспользуется

Правильная практика заключается в стабилизации структуры ключа:

  • использование примитивов вместо объектов
  • мемоизация параметров (useMemo)
  • вынесение ключей в фабрики
const userQueryKey = (id) => ['users', id];

Устаревшие данные и ложное ощущение актуальности

TanStack Query по умолчанию считает данные устаревшими сразу после получения (staleTime = 0). Это означает, что любой фокус окна или повторный маунт компонента может инициировать refetch.

Проблема возникает, когда разработчик ожидает, что кеш означает «актуальные данные», но библиотека трактует его как «данные, требующие проверки».

Типичный сценарий:

  • пользователь открывает страницу
  • данные загружаются
  • переключение вкладки браузера возвращает фокус
  • происходит повторный запрос

Это создаёт эффект:

  • мерцания UI
  • повторных загрузок
  • лишней нагрузки на API

Ошибочная настройка выглядит так:

staleTime: 0

В реальных приложениях это приводит к постоянному обновлению даже статичных данных.


Потеря кеша из-за gcTime (cacheTime)

Кеш в TanStack Query не вечен. Если запрос не используется компонентами, он удаляется через gcTime.

Проблема возникает в сценариях:

  • пользователь переходит между страницами
  • данные повторно загружаются при возврате
  • ощущение «отсутствия кеша»
gcTime: 1000 * 60 * 5

Если значение слишком маленькое:

  • кеш быстро уничтожается
  • повторные запросы становятся нормой

Если слишком большое:

  • увеличивается потребление памяти
  • сохраняются устаревшие данные

Баланс между производительностью и актуальностью становится критическим.


Конфликты инвалидирования кеша

Инвалидация (invalidateQueries) — механизм принудительного обновления данных. Однако при сложной структуре queryKey легко нарушить границы обновления.

Проблемный пример:

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

Этот вызов инвалидирует все запросы, начинающиеся с users, включая:

  • список пользователей
  • профиль пользователя
  • связанные сущности

В больших приложениях это приводит к:

  • массовым refetch
  • деградации производительности
  • лишней нагрузке на API

Обратная проблема — слишком узкая инвалидизация:

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

Если данные используются также в списках, они не обновляются, и UI начинает отображать устаревшие значения.


Несогласованность между кешем и мутациями

Мутации (useMutation) часто обновляют серверные данные, но кеш при этом остаётся неизменным.

Типичный сценарий:

mutation.mutate(data, {
  onSuccess: () => {
    queryClient.invalidateQueries(['users']);
  }
});

Проблема возникает, когда:

  • инвалидизация забыта
  • или выполнена частично
  • или происходит с задержкой

В результате UI показывает:

  • старые данные до следующего refetch
  • временную рассинхронизацию между сервером и клиентом

Особенно заметно при оптимистических обновлениях, когда кеш не синхронизирован с реальным состоянием сервера.


Устаревание данных в конкурентных запросах

При параллельных запросах TanStack Query может переопределять кеш последним завершившимся ответом.

Проблема:

useQuery({
  queryKey: ['search', query],
  queryFn: () => fetchSearch(query),
});

Если пользователь быстро меняет query:

  • запрос A начинает выполняться
  • запрос B выполняется позже
  • ответ A приходит позже B и перезаписывает кеш

Это приводит к состоянию:

  • UI показывает неактуальный результат
  • порядок ответов нарушает логическую последовательность

Перекрытие кеша при одинаковых структурах данных

Когда несколько запросов используют один и тот же queryKey, но разные queryFn, кеш становится источником конфликтов.

useQuery({
  queryKey: ['user', id],
  queryFn: fetchUserFromApiA,
});

useQuery({
  queryKey: ['user', id],
  queryFn: fetchUserFromApiB,
});

TanStack Query не различает источник данных внутри ключа. Последний успешный ответ перезаписывает предыдущий.

Это приводит к:

  • непредсказуемому состоянию данных
  • зависимости UI от порядка загрузки
  • трудностям отладки

Проблемы гидратации кеша

При серверном рендеринге (SSR) кеш предварительно заполняется через hydration. Ошибки на этом этапе приводят к расхождению между сервером и клиентом.

Типичные проблемы:

  • несовпадение queryKey
  • различие staleTime на сервере и клиенте
  • частичная гидратация

Симптомы:

  • UI перерисовывается после гидратации
  • происходит двойной запрос
  • данные «прыгают» после загрузки страницы

Разрастание кеша и утечки памяти

При большом количестве динамических ключей кеш может неконтролируемо увеличиваться.

Причины:

  • использование уникальных ключей для каждой мелкой операции
  • отсутствие переиспользования queryKey
  • длинные gcTime

В итоге:

  • увеличивается потребление памяти
  • DevTools показывает тысячи записей
  • производительность падает в долгоживущих SPA

Конфликты между staleTime и refetch стратегиями

Неправильная комбинация настроек приводит к парадоксальному поведению:

staleTime: 1000 * 60,
refetchOnWindowFocus: true

Даже при наличии «свежих» данных происходит повторный запрос при каждом фокусе окна. Это создаёт:

  • лишний сетевой трафик
  • скачки UI
  • нестабильное поведение интерфейса

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


Скрытые проблемы при предзагрузке кеша

Prefetching часто используется для ускорения навигации, но может приводить к неожиданным побочным эффектам.

queryClient.prefetchQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts,
});

Проблемы:

  • данные могут быть загружены, но никогда не использованы
  • кеш заполняется «мёртвыми» данными
  • последующий реальный запрос может перезаписать prefetched данные

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


Непредсказуемое поведение при ручном обновлении кеша

Методы setQueryData и getQueryData дают прямой доступ к кешу, но при неправильном использовании нарушают целостность данных.

queryClient.setQueryData(['user', id], old => ({
  ...old,
  name: 'updated'
}));

Проблема возникает, когда:

  • структура данных меняется со временем
  • разные компоненты используют разные ожидания от модели данных
  • кеш становится источником несогласованных структур

В больших приложениях это превращает кеш в «вторую базу данных» без строгой схемы.