Частые ошибки и их решения

Одной из самых частых ошибок при работе с TanStack Query является использование нестабильных queryKey. Ключ запроса — основа кэширования, и любое его изменение приводит к созданию нового кэша вместо переиспользования существующего.

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

useQuery({
  queryKey: ['users', { page: pageState }],
  queryFn: fetchUsers
})

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

Правильный подход — обеспечивать стабильность ключа:

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

Если структура сложная, допустимо нормализовать значения заранее:

const queryKey = useMemo(() => ['users', pageState], [pageState])

Ключевой принцип: queryKey должен быть детерминированным и примитивно сравнимым.


Дублирование запросов из-за неправильного staleTime

Частая проблема — постоянные повторные запросы при переключении компонентов или фокуса окна.

useQuery({
  queryKey: ['profile'],
  queryFn: fetchProfile
})

По умолчанию данные считаются устаревшими почти сразу, что провоцирует refetch.

Решение — управление staleTime:

useQuery({
  queryKey: ['profile'],
  queryFn: fetchProfile,
  staleTime: 1000 * 60 * 5
})

Теперь данные считаются актуальными 5 минут.

Ошибка заключается в ожидании поведения «как у Redux» — постоянного хранения без автоматической синхронизации.


Использование useEffect вместо встроенной реактивности

Типичный анти-паттерн:

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 вместо работы с кешем.


Избыточные refetch при каждом фокусе окна

По умолчанию TanStack Query может обновлять данные при возврате в окно браузера.

useQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts
})

Это приводит к неожиданным сетевым запросам.

Решение:

useQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts,
  refetchOnWindowFocus: false
})

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


Неправильное использование enabled

Частая проблема — попытка вручную контролировать выполнение запроса без понимания реактивной модели.

useQuery({
  queryKey: ['user', userId],
  queryFn: fetchUser,
  enabled: false
})

Далее запрос вызывается вручную, но теряется синхронизация зависимостей.

Правильный подход:

useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId),
  enabled: !!userId
})

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


Потеря консистентности queryKey между слоями приложения

Часто разные части приложения используют разные ключи для одних и тех же данных:

['users']
['userList']
['allUsers']

Это приводит к разрозненному кэшу и лишним запросам.

Решение — централизованная фабрика ключей:

const queryKeys = {
  users: ['users'],
  user: (id) => ['users', id]
}

Ошибка архитектурного уровня, которая проявляется как «непонятные повторные запросы».


Некорректное обновление кеша через setQueryData

Типичная ошибка — мутация существующего объекта:

queryClient.setQueryData(['users'], (old) => {
  old.push(newUser)
  return old
})

TanStack Query опирается на иммутабельность.

Правильно:

queryClient.setQueryData(['users'], (old = []) => {
  return [...old, newUser]
})

Нарушение иммутабельности приводит к отсутствию ререндера.


Потеря данных из-за неправильного garbage collection кеша

TanStack Query удаляет неиспользуемые данные через gcTime.

Ошибка возникает, когда данные исчезают неожиданно:

useQuery({
  queryKey: ['session'],
  queryFn: fetchSession,
  gcTime: 0
})

При переходах между страницами данные будут постоянно пересоздаваться.

Правильная настройка зависит от природы данных:

  • сессии — высокий gcTime
  • временные списки — низкий gcTime

Ошибки при работе с пагинацией

Частая проблема — хранение страницы вне queryKey:

useQuery({
  queryKey: ['posts'],
  queryFn: () => fetchPosts(page)
})

Это приводит к кешированию только последнего результата.

Правильно:

useQuery({
  queryKey: ['posts', page],
  queryFn: () => fetchPosts(page)
})

Ошибка здесь — разделение состояния и ключа запроса.


Неправильное использование select и потеря кеша структуры данных

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 })
}

Ошибка заключается в отсутствии контроля конкурентных запросов.


Злоупотребление refetchQueries вместо invalidateQueries

queryClient.refetchQueries(['users'])

Это принудительно инициирует запросы, даже если данные ещё актуальны.

Правильнее:

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

Ошибка — попытка использовать TanStack Query как императивный data-layer вместо декларативного кеша.


Неправильная работа с infinite queries

useInfiniteQuery({
  queryKey: ['messages'],
  queryFn: fetchMessages
})

Без корректного getNextPageParam происходит повтор загрузки одной и той же страницы.

Правильный вариант:

useInfiniteQuery({
  queryKey: ['messages'],
  queryFn: fetchMessages,
  getNextPageParam: (lastPage) => lastPage.nextCursor
})

Ошибка здесь — отсутствие понимания cursor-based pagination.


Конфликты между SSR и hydration

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

dehydrate(queryClient)

Если не синхронизировать кеш, происходит повторный fetch.

Решение — корректная гидратация:

HydrationBoundary state={dehydratedState}

Ошибка возникает из-за несоответствия ключей между сервером и клиентом.


Избыточное использование retry и “retry storm”

useQuery({
  queryKey: ['data'],
  queryFn: fetchData,
  retry: 10
})

При нестабильной сети это приводит к лавинообразным запросам.

Решение — ограничение retry:

retry: 1

или условный retry:

retry: (failureCount, error) => error.status !== 404

Потеря производительности из-за отсутствия структурного разделения кеша

Хранение больших объектов в одном queryKey приводит к лишним перерендерам:

['appData']

Любое изменение вызывает обновление всего приложения.

Лучше дробить:

['users']
['settings']
['notifications']

Ошибка — моделирование кеша как единого глобального store вместо доменной структуры.