Миграция с версии 3 на версию 4

Переход с версии 3 на 4 начинается с фундаментального изменения в экосистеме: библиотека больше не называется React Query и распространяется под именем TanStack Query.

Установка и импорт теперь строятся вокруг нового scope:

import { useQuery, useMutation, QueryClient } from '@tanstack/react-query'

Удаляется старый пакет:

npm uninstall react-query
npm install @tanstack/react-query

Дополнительно DevTools выделены в отдельный пакет:

npm install @tanstack/react-query-devtools

Ключевой момент миграции: все импорты и типы должны быть приведены к @tanstack/react-query, иначе проект будет собираться с конфликтами типов.


QueryClient и базовая конфигурация

QueryClient в v4 сохраняет общую концепцию, но становится более строгим в типизации и предсказуемым в поведении.

import { QueryClient } from '@tanstack/react-query'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: 2,
      staleTime: 0,
    },
  },
})

Важные изменения поведения

  • настройки по умолчанию стали более явно типизированными
  • часть неявных fallback-значений в v3 убрана
  • конфигурация стала более “явной”, особенно в TypeScript-проектах

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


useQuery: изменение сигнатуры queryFn

Одно из ключевых изменений v4 — унификация аргумента queryFn.

Было в v3

useQuery('todos', fetchTodos)

или

useQuery(['todos', id], ({ queryKey }) => fetchTodo(queryKey[1]))

Стало в v4

useQuery({
  queryKey: ['todos', id],
  queryFn: ({ queryKey, signal }) => fetchTodo(queryKey[1], signal),
})

Что изменилось концептуально

  • обязательный объектный синтаксис
  • queryFn всегда получает единый контекст
  • добавлен signal для отмены запросов (AbortController)
  • унифицирован доступ к queryKey

Практическое следствие

Любые старые вызовы вида:

useQuery('key', fn)

должны быть переписаны в объектный формат.


QueryKey: строгость и структура

В v4 усилилась роль структурированных ключей.

Основные требования

  • ключ всегда массив
  • строковые ключи без массива больше не поддерживаются
  • вложенные ключи считаются нормой
queryKey: ['users', userId, 'profile']

Ошибки миграции

Часто встречается:

useQuery('users') // ошибка в v4

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

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

useMutation: унификация API

useMutation также перешёл на объектную форму.

Было в v3

useMutation(createTodo, {
  onSuccess: () => {},
})

Стало в v4

useMutation({
  mutationFn: createTodo,
  onSuccess: () => {},
})

Ключевые изменения

  • mutationFn стал обязательным именованным полем
  • единый объект конфигурации для всех хуков
  • улучшенная типизация результата мутации
  • более предсказуемое поведение onSuccess, onError

Infinite queries: pageParam как стандарт

useInfiniteQuery стал более строгим в отношении pageParam.

Новый формат

useInfiniteQuery({
  queryKey: ['posts'],
  queryFn: ({ pageParam = 0 }) => fetchPosts(pageParam),
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})

Важные изменения

  • pageParam теперь всегда явно передаётся через контекст
  • обязательная обработка значения по умолчанию
  • типизация pageParam стала строже

Кэширование и поведение stale данных

Механика кэша в v4 не была радикально переписана, но поведение стало более предсказуемым.

Основные изменения

  • уменьшено количество “неочевидных” рефетчей
  • улучшено поведение при смене queryKey
  • более строгая работа с enabled

enabled как управляющий флаг

useQuery({
  queryKey: ['user', id],
  queryFn: fetchUser,
  enabled: Boolean(id),
})

В v4 enabled стал критическим инструментом предотвращения лишних запросов и чаще используется как обязательный guard.


Placeholder и сохранение данных

placeholderData и keepPreviousData остаются, но их поведение стало более очевидным.

Типовой паттерн

useQuery({
  queryKey: ['list', page],
  queryFn: fetchList,
  placeholderData: (prev) => prev,
})

Изменение логики

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

SSR и hydration

При серверном рендеринге изменения касаются в основном строгой типизации и синхронизации кэша.

Основные моменты

  • dehydrate и hydrate стали строже типизированными
  • улучшена совместимость с TypeScript
  • уменьшено количество несоответствий между сервером и клиентом
import { dehydrate, HydrationBoundary } from '@tanstack/react-query'

Devtools

Devtools выделены в отдельный пакет и подключаются явно:

import { ReactQueryDevtools } from '@tanstack/react-query-devtools'

Изменения

  • больше нет встроенного UI в основной пакет
  • Devtools подключаются только в development
  • улучшена интеграция с QueryClient

TypeScript: усиление строгой типизации

v4 значительно усилил типовую систему.

Основные последствия

  • меньше any по умолчанию
  • обязательная явная типизация в сложных случаях
  • улучшенная infer-логика для queryFn

Пример:

useQuery<User>({
  queryKey: ['user', id],
  queryFn: fetchUser,
})

Типовая ошибка миграции

Часто возникает проблема:

  • отсутствие явного типа возвращаемого значения queryFn
  • несовпадение структуры данных с ожиданиями select

Частые ошибки при миграции

1. Старый синтаксис useQuery

useQuery('key', fetchFn)

2. Отсутствие queryFn в объекте

useQuery({
  queryKey: ['key']
})

3. Неправильный queryKey

queryKey: 'users' // ошибка

4. Игнорирование signal

queryFn: () => fetch('/api') // без AbortController

Исправление:

queryFn: ({ signal }) => fetch('/api', { signal })

Изменения поведения retry

Retry остался, но стал более предсказуемым в сочетании с queryFn контекстом.

defaultOptions: {
  queries: {
    retry: (failureCount, error) => {
      return failureCount < 2
    },
  },
}

Стратегия миграции крупных проектов

Постепенное обновление зависимостей

  • сначала переход на @tanstack/react-query
  • затем замена импорта DevTools
  • далее переписывание useQuery и useMutation
  • в последнюю очередь SSR и infinite queries

Приоритеты

  1. синтаксис хуков
  2. queryFn контекст
  3. queryKey структура
  4. SSR/hydration
  5. типизация

Типичные архитектурные изменения

После перехода на v4 часто происходит рефакторинг:

  • централизованные queryKey фабрики
  • единые queryFn слои (API layer)
  • изоляция AbortController логики
  • унификация мутаций через mutation hooks

Пример фабрики ключей:

export const userKeys = {
  all: ['users'],
  detail: (id) => ['users', id],
}

Поведение при смене queryKey

v4 делает поведение более линейным:

  • смена ключа = новый кэш-запрос
  • старые данные не используются автоматически без explicit placeholderData
  • меньше “магического” поведения при повторных рендерах

Итоговые изменения ментальной модели

Миграция с v3 на v4 фактически означает переход:

  • от позиционного API к объектному
  • от неявных аргументов к контекстным объектам
  • от гибкости к строгой структуре
  • от частично скрытых механизмов к явной декларации запросов