Одной из самых частых ошибок при работе с TanStack Query является
использование нестабильных queryKey. Ключ запроса — основа
кэширования, и любое его изменение приводит к созданию нового кэша
вместо переиспользования существующего.
Проблемный паттерн:
useQuery({
queryKey: ['users', { page: pageState }],
queryFn: fetchUsers
})
На первый взгляд структура корректна, но объект внутри ключа создаётся заново при каждом рендере, что может приводить к лишним запросам.
Правильный подход — обеспечивать стабильность ключа:
useQuery({
queryKey: ['users', pageState],
queryFn: fetchUsers
})
Если структура сложная, допустимо нормализовать значения заранее:
const queryKey = useMemo(() => ['users', pageState], [pageState])
Ключевой принцип: queryKey должен быть детерминированным и примитивно сравнимым.
Частая проблема — постоянные повторные запросы при переключении компонентов или фокуса окна.
useQuery({
queryKey: ['profile'],
queryFn: fetchProfile
})
По умолчанию данные считаются устаревшими почти сразу, что провоцирует refetch.
Решение — управление staleTime:
useQuery({
queryKey: ['profile'],
queryFn: fetchProfile,
staleTime: 1000 * 60 * 5
})
Теперь данные считаются актуальными 5 минут.
Ошибка заключается в ожидании поведения «как у Redux» — постоянного хранения без автоматической синхронизации.
Типичный анти-паттерн:
const { data } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers
})
useEffect(() => {
if (data) {
setState(data)
}
}, [data])
TanStack Query уже является источником состояния. Дублирование в локальном state приводит к рассинхронизации.
Правильный подход — использовать data напрямую:
const { data } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers
})
Локальное состояние допустимо только для UI-логики, не для серверных данных.
Одна из самых критичных ошибок — отсутствие или неверная инвалидация
кеша после mutation.
const mutation = useMutation({
mutationFn: createUser
})
После создания пользователя список не обновляется автоматически.
Ошибочный подход:
mutation.mutate(data)
Правильный:
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: createUser,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ['users']
})
}
})
Ошибка часто возникает из-за попытки вручную обновлять UI вместо работы с кешем.
По умолчанию TanStack Query может обновлять данные при возврате в окно браузера.
useQuery({
queryKey: ['posts'],
queryFn: fetchPosts
})
Это приводит к неожиданным сетевым запросам.
Решение:
useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
refetchOnWindowFocus: false
})
Ошибкой считается отключение без понимания последствий. В некоторых приложениях это приводит к устаревшим данным.
Частая проблема — попытка вручную контролировать выполнение запроса без понимания реактивной модели.
useQuery({
queryKey: ['user', userId],
queryFn: fetchUser,
enabled: false
})
Далее запрос вызывается вручную, но теряется синхронизация зависимостей.
Правильный подход:
useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
enabled: !!userId
})
Ошибка заключается в том, что enabled используют как
«выключатель логики», а не как условие наличия данных.
Часто разные части приложения используют разные ключи для одних и тех же данных:
['users']
['userList']
['allUsers']
Это приводит к разрозненному кэшу и лишним запросам.
Решение — централизованная фабрика ключей:
const queryKeys = {
users: ['users'],
user: (id) => ['users', id]
}
Ошибка архитектурного уровня, которая проявляется как «непонятные повторные запросы».
Типичная ошибка — мутация существующего объекта:
queryClient.setQueryData(['users'], (old) => {
old.push(newUser)
return old
})
TanStack Query опирается на иммутабельность.
Правильно:
queryClient.setQueryData(['users'], (old = []) => {
return [...old, newUser]
})
Нарушение иммутабельности приводит к отсутствию ререндера.
TanStack Query удаляет неиспользуемые данные через
gcTime.
Ошибка возникает, когда данные исчезают неожиданно:
useQuery({
queryKey: ['session'],
queryFn: fetchSession,
gcTime: 0
})
При переходах между страницами данные будут постоянно пересоздаваться.
Правильная настройка зависит от природы данных:
Частая проблема — хранение страницы вне queryKey:
useQuery({
queryKey: ['posts'],
queryFn: () => fetchPosts(page)
})
Это приводит к кешированию только последнего результата.
Правильно:
useQuery({
queryKey: ['posts', page],
queryFn: () => fetchPosts(page)
})
Ошибка здесь — разделение состояния и ключа запроса.
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
select: (data) => data.map(u => u.name)
})
Проблема возникает, когда разные компоненты используют разные
select, что приводит к дублированию трансформаций и
непредсказуемому кэшу.
Правильнее выносить трансформации:
const selectUserNames = (data) => data.map(u => u.name)
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
select: selectUserNames
})
При быстрых переключениях страниц старые запросы продолжают выполняться:
useQuery({
queryKey: ['search', term],
queryFn: () => searchApi(term)
})
Если term меняется быстро, ответы приходят в
неправильном порядке.
Решение — поддержка signal:
const queryFn = async ({ signal }) => {
return fetch(`/api/search?q=${term}`, { signal })
}
Ошибка заключается в отсутствии контроля конкурентных запросов.
queryClient.refetchQueries(['users'])
Это принудительно инициирует запросы, даже если данные ещё актуальны.
Правильнее:
queryClient.invalidateQueries({
queryKey: ['users']
})
Ошибка — попытка использовать TanStack Query как императивный data-layer вместо декларативного кеша.
useInfiniteQuery({
queryKey: ['messages'],
queryFn: fetchMessages
})
Без корректного getNextPageParam происходит повтор
загрузки одной и той же страницы.
Правильный вариант:
useInfiniteQuery({
queryKey: ['messages'],
queryFn: fetchMessages,
getNextPageParam: (lastPage) => lastPage.nextCursor
})
Ошибка здесь — отсутствие понимания cursor-based pagination.
При серверном рендеринге часто возникает дублирование запросов на клиенте:
dehydrate(queryClient)
Если не синхронизировать кеш, происходит повторный fetch.
Решение — корректная гидратация:
HydrationBoundary state={dehydratedState}
Ошибка возникает из-за несоответствия ключей между сервером и клиентом.
useQuery({
queryKey: ['data'],
queryFn: fetchData,
retry: 10
})
При нестабильной сети это приводит к лавинообразным запросам.
Решение — ограничение retry:
retry: 1
или условный retry:
retry: (failureCount, error) => error.status !== 404
Хранение больших объектов в одном queryKey приводит к лишним перерендерам:
['appData']
Любое изменение вызывает обновление всего приложения.
Лучше дробить:
['users']
['settings']
['notifications']
Ошибка — моделирование кеша как единого глобального store вместо доменной структуры.