Использование TanStack Query для более умной загрузки данных в React

Если вы создавали React-приложения, которые взаимодействуют с API, вы знаете этот паттерн: useEffect
для загрузки, useState
для данных, еще один useState
для состояния загрузки, возможно, еще один для ошибок. И вот вы уже управляете запутанным клубком состояний только для того, чтобы отобразить список пользователей.
Такой ручной подход работает, но он хрупкий. Что происходит, когда пользователь уходит со страницы и возвращается? Перезагружать данные? Использовать устаревшие данные? А что насчет повторных попыток при сбоях сети? Это не крайние случаи — это реальность продакшн-приложений.
TanStack Query (ранее React Query) решает эти проблемы, рассматривая серверное состояние отдельно от клиентского. Вместо императивной загрузки данных вы декларативно описываете, что вам нужно, и позволяете библиотеке обрабатывать кэширование, синхронизацию и обновления. Эта статья покажет, как перейти от ручной загрузки данных к более умному, декларативному подходу, который масштабируется.
Ключевые выводы
- Замените ручные паттерны
useEffect
+useState
декларативными хукамиuseQuery
для более чистого и поддерживаемого кода - Используйте автоматическое кэширование, фоновую перезагрузку и дедупликацию запросов для улучшения пользовательского опыта
- Применяйте мутации с оптимистичными обновлениями для создания отзывчивых интерфейсов, которые кажутся мгновенными
- Реализуйте правильные стратегии инвалидации запросов для поддержания синхронизации данных в приложении
- Избегайте распространенных ошибок, таких как использование TanStack Query для локального состояния или забывание динамических параметров в ключах запросов
Проблема с ручной загрузкой данных
Вот типичный React-компонент, загружающий данные:
import { useState, useEffect } from 'react';
function UserList() {
const [users, setUsers] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
async function fetchUsers() {
setIsLoading(true);
setError(null);
try {
const response = await fetch('/api/users');
if (!response.ok) throw new Error('Failed to fetch');
const data = await response.json();
if (!cancelled) {
setUsers(data);
}
} catch (err) {
if (!cancelled) {
setError(err.message);
}
} finally {
if (!cancelled) {
setIsLoading(false);
}
}
}
fetchUsers();
return () => { cancelled = true };
}, []);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Это работает, но посмотрите, чем мы управляем:
- Три отдельных части состояния
- Логика очистки для предотвращения обновлений состояния в размонтированных компонентах
- Отсутствие кэширования — каждое монтирование запускает новую загрузку
- Отсутствие логики повторных попыток для неудачных запросов
- Отсутствие способа узнать, устарели ли наши данные
Теперь умножьте эту сложность на десятки компонентов. Вот где на помощь приходит TanStack Query.
Как TanStack Query делает загрузку данных умнее
TanStack Query рассматривает серверное состояние как гражданина первого класса. Вместо ручной оркестровки загрузок вы декларативно описываете требования к данным:
import { useQuery } from '@tanstack/react-query';
function UserList() {
const { data: users, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(res => {
if (!res.ok) throw new Error('Failed to fetch');
return res.json();
}),
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{users?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Та же функциональность, но обратите внимание на то, чего нет:
- Нет
useState
для данных, загрузки или ошибок - Нет
useEffect
или логики очистки - Нет ручной синхронизации состояния
Но вот что вы получаете:
- Автоматическое кэширование: уйдите и вернитесь — увидите мгновенные результаты
- Фоновая перезагрузка: устаревшие данные обновляются незаметно
- Дедупликация запросов: несколько компонентов могут использовать один запрос
- Встроенная логика повторов: неудачные запросы повторяются автоматически
- Оптимистичные обновления: интерфейс обновляется до подтверждения сервера
Основные концепции для умного управления данными
Запросы: чтение серверного состояния
Запросы загружают и кэшируют данные. Хук useQuery
принимает объект конфигурации с двумя основными свойствами:
const { data, isLoading, error } = useQuery({
queryKey: ['todos', userId, { status: 'active' }],
queryFn: () => fetchTodosByUser(userId, { status: 'active' }),
staleTime: 5 * 60 * 1000, // 5 минут
gcTime: 10 * 60 * 1000, // 10 минут (ранее cacheTime)
});
Ключи запросов уникально идентифицируют каждый запрос. Это массивы, которые могут включать:
- Статические идентификаторы:
['users']
- Динамические параметры:
['user', userId]
- Сложные фильтры:
['todos', { status, page }]
Когда любая часть ключа запроса изменяется, TanStack Query знает, что нужно загрузить свежие данные.
Мутации: изменение серверного состояния
В то время как запросы читают данные, мутации их изменяют:
import { useMutation, useQueryClient } from '@tanstack/react-query';
function CreateTodo() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (newTodo) =>
fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo),
headers: { 'Content-Type': 'application/json' },
}).then(res => {
if (!res.ok) throw new Error('Failed to create todo');
return res.json();
}),
onSuccess: () => {
// Инвалидировать и перезагрузить
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
return (
<button
onClick={() => mutation.mutate({ title: 'New Todo' })}
disabled={mutation.isPending}
>
{mutation.isPending ? 'Creating...' : 'Create Todo'}
</button>
);
}
Мутации обрабатывают полный жизненный цикл: состояния загрузки, обработку ошибок и колбэки успеха. Колбэк onSuccess
идеально подходит для обновления кэша после изменений.
Инвалидация запросов: поддержание свежести данных
Инвалидация помечает запросы как устаревшие, запуская фоновые перезагрузки:
// Инвалидировать всё
queryClient.invalidateQueries();
// Инвалидировать конкретные запросы
queryClient.invalidateQueries({ queryKey: ['todos'] });
// Инвалидировать с точным совпадением
queryClient.invalidateQueries({
queryKey: ['todo', 5],
exact: true
});
Так TanStack Query поддерживает синхронизацию интерфейса после мутаций. Изменили задачу? Инвалидируйте список задач. Обновили пользователя? Инвалидируйте данные этого пользователя.
Настройка TanStack Query в React-приложении
Сначала установите пакет:
npm install @tanstack/react-query
Затем оберните приложение провайдером QueryClient:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 1 минута
gcTime: 5 * 60 * 1000, // 5 минут (ранее cacheTime)
retry: 3,
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
</QueryClientProvider>
);
}
QueryClient
управляет всем кэшированием и синхронизацией. Обычно вы создаете один экземпляр для всего приложения.
Продвинутые паттерны для продакшн-приложений
Оптимистичные обновления
Обновите интерфейс немедленно, затем синхронизируйтесь с сервером:
const mutation = useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
// Отменить исходящие перезагрузки
await queryClient.cancelQueries({ queryKey: ['todos'] });
// Сохранить предыдущее значение
const previousTodos = queryClient.getQueryData(['todos']);
// Оптимистично обновить
queryClient.setQueryData(['todos'], old =>
old ? [...old, newTodo] : [newTodo]
);
// Вернуть контекст для отката
return { previousTodos };
},
onError: (err, newTodo, context) => {
// Откатить при ошибке
if (context?.previousTodos) {
queryClient.setQueryData(['todos'], context.previousTodos);
}
},
onSettled: () => {
// Всегда перезагружать после ошибки или успеха
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
Зависимые запросы
Создавайте цепочки запросов, которые зависят друг от друга:
const { data: user } = useQuery({
queryKey: ['user', email],
queryFn: () => getUserByEmail(email),
});
const { data: projects } = useQuery({
queryKey: ['projects', user?.id],
queryFn: () => getProjectsByUser(user.id),
enabled: !!user?.id, // Запускать только когда ID пользователя существует
});
Бесконечные запросы
Обрабатывайте пагинированные данные с бесконечной прокруткой:
import { useInfiniteQuery } from '@tanstack/react-query';
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['projects'],
queryFn: ({ pageParam = 0 }) => fetchProjects({ page: pageParam }),
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
initialPageParam: 0,
});
Распространенные ошибки и решения
Забывание QueryClientProvider
Если ваши запросы возвращают undefined или вообще не работают, проверьте, что приложение обернуто в QueryClientProvider
. Это самая распространенная ошибка настройки.
Использование TanStack Query для локального состояния
TanStack Query предназначен для серверного состояния. Не используйте его для полей форм, переключателей интерфейса или другого состояния только клиента. Придерживайтесь useState
или useReducer
для таких случаев.
Статические ключи запросов с динамическими данными
Всегда включайте динамические параметры в ключи запросов:
// ❌ Неправильно - будет показывать одни данные для всех пользователей
useQuery({
queryKey: ['user'],
queryFn: () => fetchUser(userId),
});
// ✅ Правильно - уникальная запись в кэше для каждого пользователя
useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
Заключение
TanStack Query преобразует загрузку данных в React из ручного, подверженного ошибкам процесса в декларативную, надежную систему. Рассматривая серверное состояние как принципиально отличное от клиентского, он устраняет шаблонный код, добавляя мощные функции, такие как кэширование, синхронизация и оптимистичные обновления.
Ментальный сдвиг прост: перестаньте думать о том, как загружать данные, и начните думать о том, какие данные вам нужны. Декларируйте свои требования через ключи и функции запросов, затем позвольте TanStack Query справиться со сложностью поддержания всего в синхронизации.
Начните с малого — замените одну загрузку useEffect
на useQuery
. Как только вы увидите мгновенные состояния загрузки, автоматические повторы и бесшовное кэширование в действии, вы поймете, почему TanStack Query стал незаменимым для современных React-приложений.
Часто задаваемые вопросы
Это одна и та же библиотека. React Query был переименован в TanStack Query, чтобы отразить, что теперь он поддерживает множество фреймворков помимо React. React-специфичный пакет все еще поддерживается и использует тот же API, с которым вы знакомы.
Да, но вам, вероятно, не нужно для серверного состояния. TanStack Query автоматически обрабатывает кэширование и совместное использование данных между компонентами. Используйте Redux или Context для истинного клиентского состояния, такого как пользовательские настройки или состояние интерфейса, и TanStack Query для всего, что приходит из API.
По умолчанию запросы перезагружаются при фокусе окна, переподключении и монтировании. Вы контролируете свежесть с помощью staleTime (как долго данные остаются свежими) и gcTime (как долго хранить неиспользуемые данные). Устаревшие запросы перезагружаются в фоне, мгновенно показывая кэшированные данные.
Инвалидация помечает запросы как устаревшие, но не перезагружает немедленно — ждет, пока запрос снова не будет использован. Прямая перезагрузка запускает немедленный сетевой запрос. Используйте инвалидацию после мутаций для лучшей производительности, так как она перезагружает только запросы, которые действительно отображаются.
TanStack Query отлично справляется с обновлениями почти в реальном времени через polling и интервалы перезагрузки. Для истинных потребностей реального времени комбинируйте его с WebSockets: используйте сокет для живых обновлений и TanStack Query для начальной загрузки данных и резерва при отключении сокета.
Да. TanStack Query 5+ полностью совместим с React 19. API, используемые здесь (запросы, мутации, инвалидация), не изменились, поэтому примеры в этой статье будут работать без изменений.