TanStack Query скрывает значительную часть сложности работы с серверным состоянием: кэширование, синхронизацию, повторные запросы, инвалидацию, фоновое обновление, дедупликацию и управление жизненным циклом запросов. Именно поэтому многие ошибки оказываются неочевидными. Проблема может находиться не в самом запросе, а в ключах, staleTime, повторных рендерах, пересоздании QueryClient или неправильной работе invalidateQueries.
Отладка в TanStack Query строится вокруг нескольких ключевых направлений:
Пакет Devtools предоставляет визуальный интерфейс для анализа состояния запросов.
Установка:
npm install @tanstack/react-query-devtools
Подключение:
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
function App() {
return (
<>
<Routes />
<ReactQueryDevtools initialIsOpen={false} />
</>
)
}
После подключения появляется панель, отображающая:
Каждый query проходит несколько состояний:
const query = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
})
Состояния:
query.status
Возможные значения:
Дополнительные флаги:
query.isPending
query.isLoading
query.isFetching
query.isError
query.isSuccess
query.isRefetching
Очень распространённая ошибка — неправильная интерпретация этих флагов.
Активен только при первом запросе.
if (query.isLoading) {
return <Spinner />
}
Активен при любом сетевом запросе.
if (query.isFetching) {
console.log('Идёт обновление')
}
Ситуация:
staleTime: 0
При возвращении во вкладку браузера:
query.isFetching === true
Но:
query.isLoading === false
Данные уже существуют в кэше.
Одна из наиболее частых проблем.
Причины:
Проблемный пример:
useQuery({
queryKey: ['users', { page: currentPage }],
queryFn: fetchUsers,
})
Если объект создаётся заново на каждом рендере:
{ page: currentPage }
может происходить лишняя сериализация и повторные вычисления.
Опасный вариант:
queryKey: ['users', filters]
где filters постоянно пересоздаётся.
Правильный подход:
const filters = useMemo(() => ({
page: currentPage,
}), [currentPage])
useQuery({
queryKey: ['users', filters],
queryFn: fetchUsers,
})
Критическая ошибка:
function App() {
const queryClient = new QueryClient()
return (
<QueryClientProvider client={queryClient}>
<Routes />
</QueryClientProvider>
)
}
Каждый ререндер создаёт новый кэш.
Симптомы:
Правильно:
const queryClient = new QueryClient()
function App() {
return (
<QueryClientProvider client={queryClient}>
<Routes />
</QueryClientProvider>
)
}
Devtools позволяет увидеть:
Часто проблема скрыта именно в несовпадении ключей.
Пример:
['user', 1]
и
['users', 1]
Это два разных query.
Очень частая проблема — invalidateQueries не обновляет данные.
Ошибка:
queryClient.invalidateQueries({
queryKey: ['users'],
})
Но реальный queryKey:
['users', 'list']
или:
['user']
Необходимо проверять:
queryClient.invalidateQueries({
queryKey: ['users'],
exact: true,
})
Будет инвалидирован только:
['users']
Но не:
['users', 1]
Получение query из кэша:
const state = queryClient.getQueryState(['users'])
Проверка:
console.log(state)
Можно увидеть:
Получение данных:
const data = queryClient.getQueryData(['users'])
Все queries:
const queries = queryClient.getQueryCache().getAll()
Полезно при сложной диагностике.
Инструмент глубокой диагностики.
queryClient.getQueryCache().subscribe((event) => {
console.log(event)
})
Позволяет отслеживать:
По умолчанию TanStack Query выполняет retry.
retry: 3
Симптом:
Вкладка Network показывает несколько одинаковых запросов.
Разработчик ошибочно считает, что приложение отправляет лишние запросы.
Проверка:
failureCount
или Devtools.
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
retry: false,
})
Упрощает анализ ошибок.
Ошибки staleTime встречаются постоянно.
staleTime: 0
Запрос считается устаревшим сразу.
Следствия:
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
refetchOnWindowFocus: true,
})
При переключении вкладок происходят запросы.
Часто воспринимается как баг.
Для проверки:
refetchOnWindowFocus: false
Ранее параметр назывался cacheTime.
gcTime: 1000 * 60 * 5
Если время слишком маленькое:
gcTime: 0
Кэш удаляется почти мгновенно.
Симптомы:
Devtools отображает mutations отдельно от queries.
Можно анализировать:
Ошибка:
onMutate: async () => {
queryClient.setQueryData(...)
}
Но rollback отсутствует.
Правильный подход:
onMutate: async () => {
const previous = queryClient.getQueryData(['todos'])
queryClient.setQueryData(['todos'], updater)
return { previous }
},
onError: (error, variables, context) => {
queryClient.setQueryData(
['todos'],
context.previous
)
}
Ситуация:
TanStack Query частично решает проблему через cancellation.
queryFn: async ({ signal }) => {
const response = await fetch('/api/users', {
signal,
})
return response.json()
}
Если signal игнорируется, отмена не работает.
Проблема:
select: (data) => ({
...data
})
Каждый вызов создаёт новый объект.
Следствия:
Лучше:
select: (data) => data.items
Полезно использовать:
console.count('Component render')
или React DevTools Profiler.
Иногда компонент ререндерится из-за изменения внутренних свойств query.
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
notifyOnChangeProps: ['data'],
})
Компонент будет реагировать только на изменения data.
Частая ошибка:
enabled: !!userId
Но:
userId = 0
Запрос не выполняется.
Лучше:
enabled: userId !== undefined
Проблема:
const userQuery = useQuery(...)
const postsQuery = useQuery(...)
postsQuery стартует раньше userQuery.
Правильно:
const postsQuery = useQuery({
queryKey: ['posts', userQuery.data?.id],
queryFn: fetchPosts,
enabled: !!userQuery.data,
})
При SSR возможны ошибки:
Проверка:
dehydrate(queryClient)
и:
<Hydrate state={pageProps.dehydratedState}>
Плохой queryKey:
queryKey: ['users', new Date()]
или:
queryKey: ['users', function() {}]
Ключи должны быть сериализуемыми.
Создание кастомного logger:
const queryClient = new QueryClient({
logger: {
log: console.log,
warn: console.warn,
error: console.error,
},
})
Полезно при сложной отладке.
Полезно анализировать:
error.message
или:
error.response
если используется axios.
const queryClient = new QueryClient({
defaultOptions: {
queries: {
onError: (error) => {
console.error(error)
},
},
},
})
Проблема:
useQuery({
queryKey: ['users'],
})
и:
useQuery({
queryKey: ['users '],
})
Лишний пробел создаёт новый query.
Devtools показывает количество observers.
Если observers:
0
query становится inactive.
Позже может быть удалён gc collector.
query.fetchStatus
Возможные значения:
Это отдельное состояние от status.
Если запросы зависают:
fetchStatus === 'paused'
Возможна проблема networkMode.
networkMode: 'always'
или:
networkMode: 'offlineFirst'
Неправильная конфигурация может блокировать запросы.
Для глубокой диагностики:
import { setLogger } from '@tanstack/react-query'
setLogger({
log: console.log,
warn: console.warn,
error: console.error,
})
Проблема:
Проверка:
queryClient.getQueryCache().getAll()
Причина часто связана с динамическими queryKey:
['users', Date.now()]
Создаётся бесконечное количество query.
Типичная ошибка:
getNextPageParam: () => true
Бесконечная пагинация никогда не заканчивается.
Правильно:
getNextPageParam: (lastPage) => {
return lastPage.nextCursor
}
refetchInterval: 1000
Запрос выполняется каждую секунду.
Иногда разработчик забывает отключить polling.
Ошибка:
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ['todo']
})
}
Но query использует:
['todos']
Кэш не обновляется.
При использовании Suspense ошибки могут скрываться внутри boundary.
Важно анализировать:
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary onRe set={reset}>
<Page />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
Позволяет корректно сбрасывать ошибки запросов.
query.dataUpdatedAt
Позволяет определить:
Ситуация:
setQueryData()
обновил кэш, но UI не изменился.
Причины:
Проблемный пример:
oldData.items.push(newItem)
return oldData
Правильно:
return {
...oldData,
items: [...oldData.items, newItem],
}
TanStack Query автоматически объединяет одинаковые запросы.
Если deduplication не работает, причины обычно:
Важно сопоставлять:
Именно Network часто показывает реальную картину поведения приложения.
Практический порядок анализа: