TanStack Query строит свою модель вокруг идеи локального кеша как
единственного источника состояния для серверных данных. Это означает,
что любой запрос (query) после выполнения сохраняется в
памяти клиента и может быть переиспользован без повторного обращения к
серверу. Такой подход уменьшает количество сетевых запросов, ускоряет
интерфейс и делает поведение данных предсказуемым.
Однако именно кеширование становится источником сложных и часто неочевидных проблем. Они возникают не из-за ошибок библиотеки, а из-за неправильного понимания жизненного цикла данных, ключей запросов и стратегий инвалидирования.
Ключевые сущности, влияющие на кеш:
queryKey — идентификатор данныхstaleTime — время актуальностиcacheTime (в новых версиях gcTime) — время
жизни неиспользуемого кешаqueryClient — центральное хранилище состоянияОшибки в любой из этих частей приводят к рассинхронизации UI и сервера.
Одной из самых частых проблем является некорректное формирование
ключей запросов. TanStack Query считает данные одинаковыми только при
полном совпадении queryKey.
Проблема возникает, когда ключи формируются неструктурированно:
useQuery({
queryKey: ['users', id],
queryFn: () => fetchUser(id),
});
useQuery({
queryKey: ['users', String(id)],
queryFn: () => fetchUser(id),
});
На уровне логики это один и тот же запрос, но для кеша это два разных набора данных. В результате:
Особенно критично это при динамических фильтрах:
queryKey: ['products', { category, page }]
Если объект создаётся без стабилизации (например, новые ссылки на каждый рендер), кеш начинает фрагментироваться.
TanStack Query использует глубокое сравнение ключей, но не нормализует объекты автоматически. Это приводит к ситуации, когда одинаковые по содержанию ключи считаются разными.
queryKey: ['items', { filter: { status: 'active' } }]
Если на каждом рендере создаётся новый объект filter,
кеш теряет эффективность.
Типичный симптом:
Правильная практика заключается в стабилизации структуры ключа:
useMemo)const userQueryKey = (id) => ['users', id];
TanStack Query по умолчанию считает данные устаревшими сразу после
получения (staleTime = 0). Это означает, что любой фокус
окна или повторный маунт компонента может инициировать refetch.
Проблема возникает, когда разработчик ожидает, что кеш означает «актуальные данные», но библиотека трактует его как «данные, требующие проверки».
Типичный сценарий:
Это создаёт эффект:
Ошибочная настройка выглядит так:
staleTime: 0
В реальных приложениях это приводит к постоянному обновлению даже статичных данных.
Кеш в TanStack Query не вечен. Если запрос не используется
компонентами, он удаляется через gcTime.
Проблема возникает в сценариях:
gcTime: 1000 * 60 * 5
Если значение слишком маленькое:
Если слишком большое:
Баланс между производительностью и актуальностью становится критическим.
Инвалидация (invalidateQueries) — механизм
принудительного обновления данных. Однако при сложной структуре queryKey
легко нарушить границы обновления.
Проблемный пример:
queryClient.invalidateQueries({ queryKey: ['users'] });
Этот вызов инвалидирует все запросы, начинающиеся с
users, включая:
В больших приложениях это приводит к:
Обратная проблема — слишком узкая инвалидизация:
queryClient.invalidateQueries({ queryKey: ['users', id] });
Если данные используются также в списках, они не обновляются, и UI начинает отображать устаревшие значения.
Мутации (useMutation) часто обновляют серверные данные,
но кеш при этом остаётся неизменным.
Типичный сценарий:
mutation.mutate(data, {
onSuccess: () => {
queryClient.invalidateQueries(['users']);
}
});
Проблема возникает, когда:
В результате UI показывает:
Особенно заметно при оптимистических обновлениях, когда кеш не синхронизирован с реальным состоянием сервера.
При параллельных запросах TanStack Query может переопределять кеш последним завершившимся ответом.
Проблема:
useQuery({
queryKey: ['search', query],
queryFn: () => fetchSearch(query),
});
Если пользователь быстро меняет query:
Это приводит к состоянию:
Когда несколько запросов используют один и тот же
queryKey, но разные queryFn, кеш становится
источником конфликтов.
useQuery({
queryKey: ['user', id],
queryFn: fetchUserFromApiA,
});
useQuery({
queryKey: ['user', id],
queryFn: fetchUserFromApiB,
});
TanStack Query не различает источник данных внутри ключа. Последний успешный ответ перезаписывает предыдущий.
Это приводит к:
При серверном рендеринге (SSR) кеш предварительно
заполняется через hydration. Ошибки на этом этапе приводят к расхождению
между сервером и клиентом.
Типичные проблемы:
queryKeystaleTime на сервере и клиентеСимптомы:
При большом количестве динамических ключей кеш может неконтролируемо увеличиваться.
Причины:
gcTimeВ итоге:
Неправильная комбинация настроек приводит к парадоксальному поведению:
staleTime: 1000 * 60,
refetchOnWindowFocus: true
Даже при наличии «свежих» данных происходит повторный запрос при каждом фокусе окна. Это создаёт:
При этом слишком большой staleTime может полностью
отключить автоматическое обновление данных, создавая иллюзию устаревшего
UI.
Prefetching часто используется для ускорения навигации, но может приводить к неожиданным побочным эффектам.
queryClient.prefetchQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
});
Проблемы:
Особенно критично при массовом предзагрузке страниц, где большинство данных не открывается пользователем.
Методы setQueryData и getQueryData дают
прямой доступ к кешу, но при неправильном использовании нарушают
целостность данных.
queryClient.setQueryData(['user', id], old => ({
...old,
name: 'updated'
}));
Проблема возникает, когда:
В больших приложениях это превращает кеш в «вторую базу данных» без строгой схемы.