← назад к разделу

Практически любое React-приложение работает с данными с сервера: список товаров, профиль пользователя, история заказов. Казалось бы, задача простая — сделал запрос, положил данные в переменную, отобразил. Но на практике всё сложнее: нужно показать индикатор загрузки, обработать ошибку, не делать лишних запросов, обновить список после изменения. Всё это можно написать самому — и это будет много кода с багами. Или воспользоваться TanStack Query.

Почему fetch в useEffect не работает хорошо

Самый очевидный способ загрузить данные в React — сделать запрос внутри useEffect и сохранить результат в useState:

function ProductList() {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch("/api/products")
      .then((r) => r.json())
      .then(setData);
  }, []);

  return <ul>{data?.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;
}

Такой код работает для самого простого случая, но быстро показывает проблемы:

  • Нет индикатора загрузки. Пока данные не пришли, компонент отображает пустоту.
  • Нет обработки ошибки. Если запрос упал, пользователь ничего не увидит.
  • Нет кеша. Каждый раз, когда компонент монтируется заново, идёт новый запрос — даже если данные уже есть.
  • Гонка запросов. Если пользователь быстро переключает фильтры, несколько запросов летят параллельно и приходят в случайном порядке — в итоге на экране оказываются устаревшие данные.
  • Дублирующиеся запросы. Если один и тот же список нужен двум компонентам, каждый делает свой запрос независимо.

TanStack Query (раньше назывался React Query) решает все эти проблемы из коробки.

Что такое TanStack Query

TanStack Query — библиотека для управления серверным состоянием в React. Главная идея: данные с сервера — это не часть состояния приложения, а кеш. Библиотека управляет этим кешем: загружает данные, хранит их, обновляет в фоне, инвалидирует когда нужно.

Установка:

npm install @tanstack/react-query

Приложение оборачивается в провайдер один раз:

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Router />
    </QueryClientProvider>
  );
}

Как сделать запрос через useQuery

Любой запрос данных объявляется через useQuery. У него два обязательных параметра:

  • queryKey — уникальный ключ, по которому Query хранит данные в кеше. Если два компонента используют один ключ, они получат одни и те же данные без повторного запроса.
  • queryFn — асинхронная функция, которая загружает данные. Должна вернуть данные или выбросить ошибку.
import { useQuery } from "@tanstack/react-query";

function useProducts() {
  return useQuery({
    queryKey: ["products"],
    queryFn: async (): Promise<Product[]> => {
      const res = await fetch("/api/products");
      if (!res.ok) throw new Error("Не удалось загрузить товары");
      return res.json();
    },
  });
}

Хорошая практика — выносить useQuery в отдельный хук (useProducts, useUser и т.д.), а не писать прямо в компоненте. Тогда логику загрузки легко переиспользовать.

В компоненте используем то, что вернул хук:

function ProductList() {
  const { data, isPending, isError } = useProducts();

  if (isPending) return <Spinner />;
  if (isError) return <p>Что-то пошло не так. Попробуйте обновить страницу.</p>;

  return (
    <ul>
      {data.map((p) => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  );
}

isPending — запрос ещё идёт, данных нет. isError — запрос завершился ошибкой. data — загруженные данные (доступны только когда isPending и isError оба false).

Параметры в ключе запроса

Если данные зависят от параметра (категория, поисковый запрос, страница) — параметр включается в queryKey. Query автоматически делает новый запрос при изменении ключа:

function useProducts(category: string) {
  return useQuery({
    queryKey: ["products", category],
    queryFn: async (): Promise<Product[]> => {
      const res = await fetch(`/api/products?category=${category}`);
      if (!res.ok) throw new Error("Ошибка загрузки");
      return res.json();
    },
  });
}

При смене категории Query запрашивает новые данные, а старые остаются в кеше — если пользователь вернётся к предыдущей категории, данные покажутся мгновенно, пока идёт фоновое обновление.

Мутации: изменение данных

useQuery — для чтения. Для создания, обновления и удаления используют useMutation.

import { useMutation, useQueryClient } from "@tanstack/react-query";

function useCreateProduct() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (input: CreateProductInput) =>
      fetch("/api/products", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(input),
      }).then((r) => {
        if (!r.ok) throw new Error("Не удалось создать товар");
        return r.json();
      }),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["products"] });
    },
  });
}

После успешного создания вызывается invalidateQueries — Query помечает кеш списка товаров устаревшим и автоматически перезапрашивает его. Список на экране обновится сам, без ручного обновления состояния.

В компоненте мутация вызывается через mutate:

function CreateProductButton() {
  const { mutate, isPending } = useCreateProduct();

  return (
    <button
      onClick={() => mutate({ name: "Новый товар", price: 100 })}
      disabled={isPending}
    >
      {isPending ? "Сохранение..." : "Создать товар"}
    </button>
  );
}

Фоновое обновление

По умолчанию TanStack Query обновляет данные в нескольких ситуациях:

  • когда пользователь возвращается на вкладку браузера;
  • когда компонент монтируется повторно;
  • когда восстанавливается интернет-соединение.

При этом пользователь видит старые данные сразу, а обновлённые подтягиваются в фоне. Это называется stale-while-revalidate — показать то, что есть, и тихо обновить.

Время, через которое данные считаются устаревшими, настраивается через staleTime:

useQuery({
  queryKey: ["products"],
  queryFn: fetchProducts,
  staleTime: 5 * 60 * 1000, // данные свежие 5 минут, фонового обновления не будет
});

Коротко

  • useEffect + useState для загрузки данных — источник целого класса багов: нет кеша, нет обработки ошибок, гонки запросов.
  • TanStack Query управляет серверными данными как кешем: загружает, хранит, обновляет в фоне.
  • useQuery объявляет запрос; queryKey — ключ кеша, queryFn — функция загрузки.
  • Параметры в queryKey — автоматическое переключение запроса при изменении фильтров.
  • isPending / isError / data — состояния запроса прямо из хука.
  • useMutation — для изменения данных; invalidateQueries после успеха обновляет связанный кеш.
  • Фоновое обновление при возврате на вкладку или восстановлении сети — из коробки.

Что почитать дальше

  • Состояние в React — разница между локальным и серверным состоянием.
  • Формы в React — как данные из запросов попадают в формы редактирования.
  • Обработка ошибок и Suspense — как показывать ошибки и состояния загрузки системно.