Исходная проблема: много данных, масса проблем
Ваше React-приложение выросло. Запросы накапливаются как снежный ком. Где хранить данные — в Redux, Context, локальном состоянии? Как синхронизировать с сервером? Кэширование, инвалидация, фоновое обновление... Всё это превращает разработку в головную боль. Неоптимальные решения приводят к мерцанию интерфейсов, лагам и раздутому коду.
Почему классические подходы терпят неудачу
- Redux/Context overkill: Ручное управление загрузкой, ошибками, обновлением данных превращает редьюсеры в монстров.
- Избыточные запросы: Компоненты независимо дергают одни и те же данные при каждом рендере.
- Несогласованность: Данные в UI расходятся с серверным состоянием после мутаций.
- Пагинация/бесконечная лента: Реализация "с нуля" требует тонкой ручной настройки.
React Query не просто кэш — это машина состояния данных:
Эта библиотека абстрагирует серверное состояние, оставляя в UI только то, что действительно нужно компонентам.
import { useQuery } from '@tanstack/react-query';
const fetchUser = async (id) => {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error('Ошибка загрузки');
return res.json();
};
function UserProfile({ userId }) {
const {
data: user,
isLoading,
error,
isFetching
} = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 5 * 60 * 1000, // 5 минут до "устаревания"
});
if (isLoading) return <Spinner />;
if (error) return <Error message={error.message} />;
return (
<div>
<h1>{user.name}</h1>
{isFetching && <small>Обновление...</small>}
</div>
);
}
Всего 10 строк, но получили:
- Кэширование между компонентами
- Фоновое обновление при повторном входе в компонент
- Индикатор фоновой загрузки
- Переиспользование одинаковых запросов
Ключевые паттерны, которые стоит взять на вооружение
Инвалидация запросов после мутаций
После изменения данных на сервере (POST/PUT/DELETE) кэш нужно помечать устаревшим. Решение: queryClient.invalidateQueries
.
const queryClient = useQueryClient();
const { mutate } = useMutation({
mutationFn: updateUser,
onSuccess: () => {
// Принудительное обновление всех запросов по ключу 'user'
queryClient.invalidateQueries({ queryKey: ['user'] });
}
});
Оптимистичные обновления
Не ждите ответа сервера — применяйте UI-изменения сразу, отменяя при ошибке:
useMutation({
mutationFn: updateUser,
onMutate: async (newUser) => {
await queryClient.cancelQueries({ queryKey: ['user', newUser.id] });
const prevUser = queryClient.getQueryData(['user', newUser.id]);
queryClient.setQueryData(['user', newUser.id], newUser); // Оптимистичный апдейт
return { prevUser }; // Для отката
},
onError: (err, newUser, context) => {
queryClient.setQueryData(['user', newUser.id], context.prevUser);
}
});
Префетчинг данных
Заранее подгружайте данные при наведении на кнопку или перед навигацией:
const queryClient = useQueryClient();
<Link
to={`/users/${id}`}
onMouseEnter={() =>
queryClient.prefetchQuery({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
})
}
>
Профиль
</Link>
Подводные камни и как их избежать
Ключи запросов — ваша новая главная абстракция
- Нормализуйте их структуру:
['resource', id, 'sub-resource', { params }]
. - Избегайте динамичных ключей без ограничений (
['posts', Date.now()]
— антипаттерн).
Стаблити и рифети: настройте тонко
staleTime
: Укажите, когда данные считаются устаревшими (по умолчанию 0).refetchInterval
: Автоперезапрос для live-данных (чаты, курсы).retry
,retryDelay
: Стратегия обработки ошибок.
Когда НЕ использовать React Query?
- Работа с чисто клиентским состоянием (чекбоксы, модалки).
- Действия без асинхронной логики (фильтрация массива).
Глубже в архитектуру
Интеграция с клиент-стэйтеми (Zustand/Jotai):
// Zustand + React Query = Синхронное состояние сервера/клиента
const useUserStore = create((set) => ({
user: null,
fetchUser: async (id) => {
const data = await queryClient.fetchQuery(['user', id]);
set({ user: data });
}
}));
Ошибка "No QueryClient set":
Оберните корневой компонент в <QueryClientProvider client={queryClient}>
.
Пагинация за 5 минут:
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey: ['projects'],
queryFn: ({ pageParam = 1 }) => fetchProjects(pageParam),
getNextPageParam: (lastPage) => lastPage.nextPage,
});
Философия React Query
Перестаньте считать данные "своими". Сервер — единственный источник истины. React Query выступает посредником: он знает, когда данные устарели, когда их нужно обновить, как минимизировать нагрузку.
Итоговый чеклист для интеграции:
- Определите уникальные ключи для всех ресурсов.
- Настройте глобальный
queryClient
(флагdefaultOptions
). - Замените
useState/useEffect
запросов наuseQuery
. - Оптимизируйте: префетчинг при ховере, стабл-тайм 2+ минуты.
- Инвалидируйте кэш после мутаций.
Вы освобождаете 30% кода от рутины. Данные становятся согласованными. Интерфейс реагирует мгновенно. Браузер меньше дергает сеть. Сервер получает меньше дублирующих запросов. Это не библиотека — это смена парадигмы управления состоянием.
Когда вы переосмыслите поток данных в приложении, окажется: большая часть проблем имеют готовые стандартные решения. Ваша задача — перестать их писать вручную и довериться инструментам.