Что такое TanStack Query (React Query) и зачем использовать?
TanStack Query (React Query)
TanStack Query (ранее React Query) — это библиотека для работы с серверным состоянием (server state) в React. Она решает задачи получения, кэширования, синхронизации и обновления данных с сервера.
Зачем нужна, если есть Redux / Zustand?
Global state-менеджеры отлично справляются с клиентским состоянием (UI-флаги, форма, выбранные фильтры), но серверное состояние имеет принципиально другую природу:
- Хранится удалённо и может устареть в любой момент
- Требует кэширования, дедупликации запросов и фоновой синхронизации
- Имеет состояния
loading,error,stale,fetching
Писать всё это вручную через useEffect + useState — много повторяющегося кода с трудно предсказуемыми edge-кейсами.
Ключевые возможности
Кэширование и stale-while-revalidate — данные кэшируются по ключу, и при повторном запросе сразу возвращается кэш, пока в фоне идёт обновление.
Автоматическое обновление — при фокусировке вкладки, восстановлении сети или по интервалу данные обновляются автоматически.
Дедупликация запросов — несколько компонентов с одним ключом запроса не создают несколько HTTP-запросов.
Инвалидация кэша — после мутации (создание, обновление, удаление) можно точечно инвалидировать нужные запросы.
Пагинация и бесконечный скролл — встроенные хуки useInfiniteQuery.
Базовый пример
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
// Получение списка пользователей
function UserList() {
const { data, isLoading, isError } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(res => res.json()),
staleTime: 1000 * 60, // данные считаются свежими 1 минуту
})
if (isLoading) return <div>Загрузка...</div>
if (isError) return <div>Ошибка загрузки</div>
return <ul>{data.map(u => <li key={u.id}>{u.name}</li>)}</ul>
}
Мутации с инвалидацией
function CreateUser() {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: (newUser) =>
fetch('/api/users', {
method: 'POST',
body: JSON.stringify(newUser),
}).then(res => res.json()),
onSuccess: () => {
// После создания инвалидируем кэш списка пользователей
queryClient.invalidateQueries({ queryKey: ['users'] })
},
})
return (
<button
onClick={() => mutation.mutate({ name: 'Иван' })}
disabled={mutation.isPending}
>
{mutation.isPending ? 'Создание...' : 'Создать пользователя'}
</button>
)
}
Настройка провайдера
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 30, // 30 секунд по умолчанию
retry: 2, // повторить запрос 2 раза при ошибке
},
},
})
function App() {
return (
<QueryClientProvider client={queryClient}>
<UserList />
</QueryClientProvider>
)
}
Когда не нужен TanStack Query
- Данные загружаются один раз при старте и не меняются (конфиги, константы)
- SSR-приложение с серверными компонентами (Next.js App Router) — там предпочтительнее fetch на сервере
- Очень простое приложение с 1–2 запросами без кэширования
Что хочет услышать интервьюер
Понимание разницы между клиентским и серверным состоянием — почему Redux не заменяет TanStack Query
Знание ключевых концепций: queryKey, staleTime, кэширование, инвалидация через invalidateQueries
Умение объяснить stale-while-revalidate и зачем нужно фоновое обновление
Практический опыт: как настроить useQuery и useMutation, как организовать QueryClient
Понимание, когда библиотека избыточна и можно обойтись без неё
Пример: Базовый useQuery с типизацией
import { useQuery } from '@tanstack/react-query'
interface User {
id: number
name: string
email: string
}
async function fetchUser(id: number): Promise<User> {
const res = await fetch(`/api/users/${id}`)
if (!res.ok) throw new Error('Ошибка загрузки пользователя')
return res.json()
}
function UserProfile({ userId }: { userId: number }) {
const { data: user, isLoading, isError, error } = useQuery<User, Error>({
queryKey: ['user', userId], // ключ зависит от userId — кэш уникален для каждого
queryFn: () => fetchUser(userId),
staleTime: 1000 * 60 * 5, // считаем данные свежими 5 минут
enabled: userId > 0, // не запускать запрос если id невалидный
})
if (isLoading) return <p>Загрузка профиля...</p>
if (isError) return <p>Ошибка: {error.message}</p>
return <div>{user?.name} — {user?.email}</div>
}
Пример: useMutation с оптимистичным обновлением
import { useMutation, useQueryClient } from '@tanstack/react-query'
interface Todo {
id: number
text: string
done: boolean
}
function TodoItem({ todo }: { todo: Todo }) {
const queryClient = useQueryClient()
const toggleMutation = useMutation({
mutationFn: (id: number) =>
fetch(`/api/todos/${id}/toggle`, { method: 'PATCH' }).then(r => r.json()),
// Оптимистичное обновление: меняем UI до ответа сервера
onMutate: async (id) => {
await queryClient.cancelQueries({ queryKey: ['todos'] })
const previous = queryClient.getQueryData<Todo[]>(['todos'])
queryClient.setQueryData<Todo[]>(['todos'], (old = []) =>
old.map(t => t.id === id ? { ...t, done: !t.done } : t)
)
return { previous } // контекст для отката
},
// Откат при ошибке
onError: (_err, _id, context) => {
if (context?.previous) {
queryClient.setQueryData(['todos'], context.previous)
}
},
// После успеха синхронизируем с сервером
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
return (
<li
style={{ textDecoration: todo.done ? 'line-through' : 'none', cursor: 'pointer' }}
onClick={() => toggleMutation.mutate(todo.id)}
>
{todo.text}
</li>
)
}
Типичные ошибки
Считают, что TanStack Query заменяет Redux/Zustand полностью — это разные инструменты для разных типов состояния
Не понимают роль queryKey: используют статические строки вместо динамических ключей с параметрами, из-за чего кэш не инвалидируется корректно
Забывают про staleTime и оставляют значение 0 — это приводит к лишним запросам при каждом рефокусе вкладки
Смешивают мутации с ручным обновлением кэша через setQueryData везде, не зная что invalidateQueries — более простое и надёжное решение
Оборачивают useQuery в useEffect, не понимая что хук сам управляет жизненным циклом запроса


