Что такое TanStack Query (React Query) и зачем использовать?

MiddleReact · Frontend·Обновлено 24 июля 2026
Коротко
TanStack Query — библиотека для управления серверным состоянием в React-приложениях, которая автоматизирует кэширование, фоновое обновление данных и синхронизацию с сервером. Она избавляет от необходимости вручную писать логику загрузки, обработки ошибок и инвалидации кэша.

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, не понимая что хук сам управляет жизненным циклом запроса

Лучшие курсы по теме

изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Feature-Sliced Design

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.5
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее