Введение: Проблема управления состоянием данных в современном фронтенде
Компоненты React по своей природе не знают, как управлять асинхронными данными. Мы привыкли комбинировать useState
для хранения данных и useEffect
для их получения - но это порождает массу проблем: неуправляемая загрузка данных при каждом рендере, дублирование запросов между компонентами, устаревший кэш, сложности синхронизации и обновлений. Долгое время управление асинхронными состояниями оставалось болевой точкой React-приложений.
React Query решает именно эти проблемы через декларативный подход к работе с серверными данными. Рассмотрим, как эта библиотека трансформирует архитектуру приложений и устраняет распространённые проблемы с производительностью.
Основные концепции React Query в действии
Кэширование по дефолту
import { useQuery } from 'react-query';
const fetchPosts = async () => {
const response = await fetch('/api/posts');
if (!response.ok) throw new Error('Network response was not ok');
return response.json();
};
function PostList() {
const { data, isLoading, error } = useQuery('posts', fetchPosts);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{data.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
Ключевое отличие от самописного решения: пользователи компонентов будут использовать один экземпляр данных вместо создания новых запросов каждый раз. React Query автоматически кэширует результаты по уникальному ключу 'posts'
и синхронизирует состояние между всеми компонентами.
Предотвращение дублирующихся запросов
Особое внимание стоит уделить механизму staleTime (время устаревания данных) и cacheTime (время хранения в кэше):
useQuery('posts', fetchPosts, {
staleTime: 5 * 60 * 1000, // 5 минут до следующей проверки обновлений
cacheTime: 30 * 60 * 1000, // данные остаются в кэше 30 минут
});
В то время как при традиционном подходе с useEffect запросы выполняются при каждом монтировании компонента, в React Query будет:
- Один запрос на все экземпляры компонентов с одним ключом
- Автоматическая проверка обновлений при возврате на страницу
- Фоновая ревалидация данных
Продвинутые паттерны производительности
Атомарные инвалидации
После мутаций данных использование ключей запросов позволяет точечно обновлять закэшированные данные:
import { useMutation, useQueryClient } from 'react-query';
function AddPost() {
const queryClient = useQueryClient();
const mutation = useMutation(newPost => axios.post('/api/posts', newPost), {
onSuccess: () => {
queryClient.invalidateQueries('posts'); // Принудительная ревалидация списка постов
queryClient.refetchQueries('analytics', { exact: true }); // Только "analytics" ключ
}
});
// форма добавления поста
}
Инвалидация происходит без полного завершения HTTP-запроса - компоненты используют текущие данные до обновления, убирая видимые задержки интерфейса.
Предварительное получение данных и позиции смещения
Для реализации бесконечных списков и пагинации без повышения задержек:
import { useInfiniteQuery } from 'react-query';
const fetchPosts = async ({ pageParam = 0 }) => {
const res = await fetch(`/api/posts?page=${pageParam}`);
return {
data: res.data,
nextPage: res.hasNextPage ? pageParam + 1 : undefined
};
};
function Posts() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage
} = useInfiniteQuery('posts', fetchPosts, {
getNextPageParam: (lastPage) => lastPage.nextPage
});
// JSX с рендером и обработкой нажатия "Подгрузить ещё"
}
React Query автоматически объединяет страницы данных и отслеживает позиции. Настройка фокуса окна (refetchOnWindowFocus) дополнительно предзагружает следующую страницу при скролле.
GraphQL и мутации с оптимистичным обновлением
Не только REST - React Query эффективен с GraphQL API через хуки useQuery
и useMutation
. Оптимистичные обновления особенно эффективны:
useMutation(updateTodo, {
onMutate: async newTodo => {
// Отменяем текущие запросы чтобы избежать перезаписи
await queryClient.cancelQueries(['todo', { id: newTodo.id }]);
// Оптимистичное обновление
const previousTodo = queryClient.getQueryData(['todo', { id: newTodo.id }]);
queryClient.setQueryData(['todo', { id: newTodo.id }], newTodo);
return { previousTodo };
},
onError: (err, newTodo, context) => {
// Откатываемся при ошибке
queryClient.setQueryData(['todo', { id: newTodo.id }], context.previousTodo);
},
onSettled: () => {
// Убеждаемся что данные актуальны
queryClient.invalidateQueries(['todo', { id: newTodo.id }]);
}
});
Интеграция с состоянием: когда использовать Redux вместе с React Query
Распространённое заблуждение: React Query заменяет всю систему управления состоянием. Это неверно. Для решения различных задач:
Задача | Инструмент |
---|---|
Серверные данные с кэшированием | React Query |
Клиентское состояние (настройки, модалки) | useState/useReducer |
Глобальное неизменяемое состояние с отслеживанием | Redux/Zustand |
Фоновые синхронизации и обновления данных | React Query |
Гибридная архитектура даст лучшие результаты — из большинства приложений можно полностью исключить целые слои сложности, перенеся ответственность за асинхронные операции на React Query.
Метрики производительности
Применение React Query в реальных проектах показывает прогнозируемое улучшение ключевых метрик:
- Уменьшение количества запросов: до 60-75% при многоэкземплярных компонентах
- Скорость интерфейса: TTFB снижается за счёт кэширования и повторного использования
- Снижения потребления памяти: комплексные объекты состояния вытесняются в отдельный менеджер с TTL
- Сокращение перерендеринга: react-query выполняет внутреннюю оптимизацию внутри useQuery
Профилирование компонентов инструментами React DevTools и Lighthouse демонстрирует сокращение блокирующего времени более чем на 30% при сохранении актуальных данных.
Чего избегать и настройки безопасности
Корректная обработка ошибок
const { isError, error } = useQuery('posts', fetchPosts, {
retry: 3,
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
onError: error => {
// Компонентная обработка
alert(`Error: ${error.message}`);
}
});
Управление поведением ошибок критично для UX. Определите политику повторных запросов и стабильный фоллбэк через global onError
в QueryClient.
SSR и Next.js интеграция
Для предварительного заполнения кваерий используем гидратацию:
import { Hydrate, QueryClient, QueryClientProvider } from 'react-query';
export default function App({ Component, pageProps }) {
const [queryClient] = React.useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>
<Hydrate state={pageProps.dehydratedState}>
<Component {...pageProps} />
</Hydrate>
</QueryClientProvider>
);
}
Сервер возвращает dehydratedState
который механизм гидратации превращает в рабочий кэш. Избегайте сетевых запросов во время выполнения гидратации используя dehydrate
и обеспечьте синхронность состояния на первичном рендере.
Выводы: новая архитектура данных
React Query смещает парадигму управления социальных в frontend-разработки. Вместо империтивных useEffect и ручной синхронизации мы получаем:
- Снижение сложности кода на 30-50% за счёт декларативного API
- Проверенные решения для распространённых проблем с производительностью
- Предотвращение распространённых архитектурных ошибок
- Автоматическая конкурентность и фоновое взаимодействие
- Плавная UX без “скачков” данных
Практическое правило: новые проекты с любого рода асинхронными операциями данных должны включать React Query или TanStack Query как основной инструмент. Для существующих приложений подход можно внедрять инкрементально, замещая самые проблемные части кодовой базы.