Введение
В Intelsy, где я работаю над различными проектами в роли веб-разработчика, мы часто сталкиваемся с задачами эффективного взаимодействия с серверными данными. В таких случаях особенно выручает React Query — библиотека для управления состоянием данных сервера. Она автоматически кэширует данные, синхронизирует их и обновляет, что избавляет разработчика от необходимости вручную управлять этим состоянием, а также снижает нагрузку на сервер.
React Query не заменяет глобальное состояние (например, Redux), а дополняет его, фокусируясь на данных, которые приходят из внешних источников.
Конечно, для получения данных с API и кэширования можно использовать стандартные средства React (useEffect и useState), но такой подход, в конечном итоге, приведёт к громоздкому коду, особенно, когда дело дойдёт до кэширования или отслеживания ошибок и статусов загрузки.
React Query хранит данные в глобальном кэше, который доступен всем компонентам приложения. Это похоже на то, как работают другие менеджеры состояний, но с фокусом на асинхронные данные. Можно выделить основные возможности библиотеки:
Кэширование данных: React Query записывает ответ на запрос в кэше по уникальному ключу и при повторном запросе просто возвращает данные из кэша, что уменьшает нагрузку на сеть и моментально выводит информацию на экран. Также есть возможность инвалидировать кэш запроса, чтобы он выполнился заново, а не загружал данные из кэша, например, после мутации с изменениями на сервере.
Фоновое обновление: библиотека может автоматически перезапрашивать данные фоново, например, при возвращении пользователя к окну приложения или по таймеру, чтобы данные оставались актуальными.
Управление статусами: React Query предоставляет встроенные флаги для отслеживания состояния запроса и имеет обработку ошибок.
Отмена и дедупликация запросов: React Query автоматически отменяет дубликаты запросов и позволяет отменять запросы при демонтировании компонента, предотвращая ненужные вызовы API.
Установка:
npm install @tanstack/react-query |
QueryClient
Для начала работы React Query требует обернуть приложение в QueryClientProvider и передать экземпляр QueryClient, который является центральным объектом в библиотеке и управляет кэшем и состоянием данных:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; |
В конструктор QueryClient можно передать объект конфигурации для базовой настройки кэширования, обновлений, ошибок, повторов и других аспектов работы с данными, но их также можно будет настроить отдельно внутри любого запроса или любой мутации.
const queryClient = new QueryClient({ |
В поле defaultOptions можно указать значения по умолчанию для запросов (queries) и мутаций (mutations). Вот некоторые настройки, относящиеся к запросам:
queryFn – функция, выполняющая запрос;
staleTime – через сколько миллисекунд данные кэша будут считаться устаревшими и React Query решит их обновить (можно даже передать Infinity и тогда данные никогда не устареют);
gcTime – через сколько миллисекунд неиспользованные данные кэша будут удалены сборщиком мусора;
retry – кол-во попыток повтора, если запрос провалится (по умолчанию 3), при этом поле может принимать как число, так и булевое значение или функцию;
retryDelay – функция или число, через сколько миллисекунд выполнять повторный запрос;
refetchOnWindowFocus – обновление данных во время возвращения на вкладку в браузере;
refetchOnReconnect – обновлять ли данные при восстановлении соединения;
refetchOnMount – обновлять ли данные при монтировании компонента;
enabled – будут ли запросы выполняться автоматически по умолчанию;
structuralSharing – функция или булевое значение для предотвращения лишних ререндеров при сверке одинаковых данных (по умолчанию true).
Настройки для мутаций:
retry, retryDelay, gcTime – практически то же самое, что и у запросов (staleTime отсутствует);
mutationFn – функция, выполняющая мутацию;
onMutate – функция, которая будет вызвана перед выполнением мутации (например, полезно для оптимистичных обновлений);
onSuccess – функция, которая будет выполнена после успешного выполнения мутации;
onError – функция, вызываемая после ошибки;
onSettled – функция, вызываемая как после успеха, так и после ошибки.
const queryClient = new QueryClient({ |
Для получения экземпляра QueryClient внутри компонентов есть специальный хук useQueryClient. Экземпляр предоставляет полезные методы для управления глобальным состоянием:
invalidateQueries – помечает определённые запросы как устаревшие, чтобы React Query выполнил их повторный вызов;
refetchQueries – немедленно выполняет повторный вызов некоторых запросов;
setQueryData – записывает данные по запросу в кэш вручную, что может пригодиться при оптимистичном обновлении или синхронизации данных после мутации;
getQueryData – чтение данных из кэша запросов;
getQueryCache – возвращает объект QueryCache для управления кэшем запросов;
getMutationCache – возвращает объект MutationCache для управления кэшем мутаций;
clear – полная очистка кэша.
const queryClient = useQueryClient(); |
QueryCache
QueryCache можно назвать хранилищем всех запросов в приложении. Таким образом, когда React Query собирается выполнить запрос, он сначала смотрит, есть ли данные в кэше запросов. Если они есть и актуальны (staleTime), то возвращаются эти данные. Если же таких данных нет или они неактуальны, то React Query выполняет запрос на сервер и сохраняет полученные данные в кэш на определённое время (gcTime) и считает их свежими до окончания staleTime.
QueryCache настраивается в QueryClient – создаётся экземпляр QueryCache и передаётся в конструктор QueryClient.
const queryCache = new QueryCache({ |
В параметры конструктора QueryCache можно передать объект с полями onSuccess, onError и onSettled, означающие то же самое, что и те же поля для мутаций в defaultOptions.mutations, только применимо к запросам.
Экземпляр QueryCache предоставляет некоторые методы для управления кэшем:
find – поиск выполненного запроса по ключу и другим параметрам;
findAll – поиск всех запросов;
get – возвращает запрос по его хэшу;
getAll – возвращает все запросы в кэше;
remove – удаляет запрос из кэша;
clear – полностью очищает кэш запросов.
// Получаем QueryCache из QueryClient
|
Таким образом, мы можем очищать кэш запросов, например, после выхода пользователя из профиля, так как эти данные нам больше не понадобятся.
MutationCache
Ещё один объект для хранения кэша, только для мутаций. Так же создаётся экземпляр MutationCache и пробрасывается в QueryClient при необходимости. Настройка MutationCache идентична QueryCache и принимает в себя те же параметры.
const mutationCache = new MutationCache({ |
Экземпляр MutationCache предоставляет немного похожие методы QueryCache, но для мутаций:
find – поиск мутации по ключу или другим параметрам;
findAll – поиск всех мутаций;
getAll – возвращает все мутации в кэше;
remove – удаляет мутацию из кэша;
clear – очищает весь кэш мутаций.
// Получаем MutationCache из QueryClient |
Запросы
Для работы с запросами React Query предлагает хук useQuery, который позволяет легко получать данные с сервера и управлять их состоянием. Вдобавок к этому, он автоматически делает кэширование, повторные запросы и обновление данных.
useQuery принимает два параметра:
options – обязательный объект с опциям для настройки запроса;
queryClient – необязательный объект для переопределения базового QueryClient.
В options передаются те же параметры, что и в defaultOptions.queries внутри QueryClient, но с тем отличием, что настройки useQuery будут переопределять базовые значения, указанные в QueryClient. Вот наиболее важные из них:
queryKey – уникальный ключ каждого запроса;
queryFn – функция для выполнения запроса;
enabled – флаг, включающий / выключающий автоматическое выполнение запроса;
select – функция, позволяющая трансформировать полученные данные;
initialData – начальные данные;
meta – дополнительная информация для запроса.
Объект options требует указать лишь одно обязательное значение queryKey – ключ запроса. React Query управляет кэшированием запросов на основе этих ключей, к тому же, если ключ меняется, запрос будет выполнен заново. Такие ключи должны быть массивом верхнего уровня и способны содержать в себе как примитивы, так и объекты. Ключ должен быть уникален для каждого создающегося запроса и включать переменные, которые будут использоваться в запросе.
const useFetchUsers = () => useQuery({ |
По ключам также можно будет получать запросы из кэша QueryCache или делать другие манипуляции, например, пометить данные как устаревшие.
Другой важный параметр – queryFn – функция, которая используется для запроса данных. Так как мы получаем данные из API, она должна быть асинхронной (но даже это не обязательно и можно создавать синхронные функции). Функция queryFn может передавать в качестве аргумента объект с полями meta, queryKey и signal.
const fetchUsers = async () => { |
Таким образом для создания запроса и его выполнения достаточно будет определить queryKey и queryFn и вызвать хук внутри компонента. Тогда запрос будет автоматически выполнен (если только не enabled = false).
const Users = () => { |
Чтобы запрос не выполнялся автоматически, надо определить параметр enabled. Это может пригодиться, когда мы хотим, чтобы запрос выполнился только при определённом условии. Либо можно вообще отключить автоматическое выполнение, передав false.
export const useFetchUser = (id: number) => useQuery({ |
Есть ещё один способ отключить автоматическое выполнение запроса, передав специальный маркер skipToken, что удобно, когда мы имеем дело с TypeScript, так как это спасает от изменения типов параметров в асинхронной функции:
export const useFetchUser = (id?: number) => useQuery({ // id не может быть undefined в fetchUser, передаём skipToken |
React Query предоставляет возможность трансформировать данные перед использованием в компоненте с помощью поля select. Функция принимает полученные с сервера данные и преобразует их в любой другой формат. Select можно даже обернуть в useCallback, если требуется оптимизация. Важно понимать, что в кэше сохраняются данные сервера, которые уже потом трансформируются через select для использования в компоненте.
const transformUsers = (users: User[]) => users.map((user) => ({ |
Используя initialData, мы можем заранее устанавливать значение до выполнения запроса, что может быть полезно в случаях, когда данные нужно показать мгновенно.
export const useFetchUser = (id: number) => { |
Meta позволяет передать в запрос дополнительные метаданные, которые затем можно будет использовать в глобальных обработчиках или логировании.
export const useFetchUsers = () => useQuery( // Формируем метаданные // Получаем метаданные |
При выполнении useQuery будет возвращать множество полезных параметров, часть из которых была в указана в примере с компонентом Users. Рассмотрим их более подробно:
data – данные последнего успешно выполненного запроса (может быть undefined);
isLoading – состояние, когда данных ещё нет в кэше, но при этом данные загружаются;
isFetching – состояние, когда данные загружаются;
isRefetching – состояние повторной загрузки данных;
isSuccess – состояние успешного выполнения запроса;
isPaused – состояние, когда выполнение запроса приостановлено;
isStale – состояние, когда данные в кэше устарели;
isError – состояние ошибки;
error – объект ошибки или null;
isLoadingError – состояние ошибки при первом вызове запроса;
isRefetchError – состояние ошибки при повторном вызове запроса;
status – одно из трёх состояний запроса “error” (isError), “success” (isSuccess) или “pending” (isPending);
fetchStatus – одно из трёх состояний загрузки “fetching” (isFetching), “idle” (запрос не выполняется) или “paused” (isPaused);
failureCount – кол-во проваленных запросов;
refetch – функция для выполнения запроса заново или запуска вручную.
По запросам можно подвести простой итог:
В первый раз React Query автоматически выполняет запрос, если enabled = true либо запрос выполняется вручную через refetch.
Если произошла ошибка, то библиотека попробует выполнить его заново указанное в retry кол-во раз.
После успешного получения данных они сохраняются в кэш.
Пока данные свежие (staleTime), React Query будет просто брать данные из кэша вместо выполнения запроса, например, если компонент был смонтирован заново или если вызван этот же запрос в другом компоненте.
Как только данные устареют, библиотека выполнит запрос заново в фоновом режиме;
Если данные не будут использованы, то по истечении gcTime они вовсе удаляются из кэша и весь алгоритм начнётся заново.
Запросы с пагинацией
Для удобной работы с большими наборами данных, React Query предоставляет отдельный хук useInfiniteQuery. Но, конечно, сперва на самом сервере должна быть настроена пагинация. Использование этого хука практически ничем не отличается от useQuery, кроме того, что он позволяет удобнее запрашивать данные порциями.
В настройке появляются поля initialPageParam, getNextPageParam, getPreviousPageParam, а queryFn теперь позволяет брать аргумент pageParam и прокидывать в функцию запроса.
// Запрос с заложенной пагинацией // Берём массив страниц и применяем трансформирование для каждой |
То есть, в данной реализации, для каждой порции данных с сервера приходит ещё два поля nextPage и prevPage, которые в дальнейшем мы используем для получения следующей и предыдущей страницы соответственно. При получении данных из каждой порции хук сам группирует полученные данные в массив из страниц, который затем и кэширует.
Хук возвращает объект с теми же полями, что и useQuery, но к ним добавились ещё:
isFetchingNextPage – состояние загрузки следующей порции данных;
isFetchingPreviousPage – состояние загрузки предыдущей порции данных;
isFetchNextPageError – состояние ошибки при получении следующей порции данных;
isFetchPreviousPageError – состояние ошибки при получении предыдущей порции данных;
hasNextPage – имеется ли возможность получить следующую порцию данных (рассчитывается на основе getNextPageParam);
hasPreviousPage – имеется ли возможность получить предыдущую порцию данных (рассчитывается на основе getPreviousPageParam);
fetchNextPage – загрузить следующую порцию данных;
fetchPreviousPage – загрузить предыдущую порцию данных;
const Posts = () => { |
В целом, для постраничных запросов может подойти и обычный useQuery с динамическим queryKey, однако в случае загрузки в бесконечном скролле удобнее использовать useInfiniteQuery.
Параллельные запросы
Бывают случаи, когда необходимо выполнить несколько запросов параллельно. Для этого React Query предлагает хук useQueries.
useQueries принимает массив конфигураций и возвращает массив результатов (аналогично useQuery, но для нескольких запросов).
export const useFetchProfileInfo = () => useQueries({ |
К тому же useQueries позволяет выполнять параллельные запросы прямо из массива.
const userIds = [1, 2, 3]; |
Так как useQueries возвращает массив, статусы нужно проверять для каждого запроса отдельно.
// Получаем массив запросов const usersQueries = useFetchUsers(); |
Что удобно, можно определить, какой объект будет возвращать хук с помощью поля combine.
export const useFetchUsers = () => useQueries({ // Формируем свой объект с data и isSuccess |
// Получаем объект, определённый в combine const { data, isSuccess } = useFetchUsers(); |
Мутации
Мутации предназначены для изменений данных на сервере (как правило, PUT, POST, PATCH и DELETE запросы). То есть, в отличие от useQuery, хук useMutation предоставляет инструменты для модификации данных.
useMutation принимает в себя два аргумента:
options – обязательный объект для настройки мутации;
queryClient – необязательный экземпляр для переопределения базового QueryClient.
const editUser = async (id: number, name: string) => { |
В данном примере мы используем запрос для изменения имени пользователя. В mutationFn мы определяем, какие аргументы будут пробрасываться в сам запрос (в отличие от useQuery, где аргументы определяются на уровне хука). Далее мы используем поле onSuccess для того, чтобы произвести какие-либо действия после успешного выполнения мутации. Я хочу, чтобы после изменения имени, данные в кэше по этому пользователю обновились, поэтому я помечаю их как устаревшие через экземпляр queryClient. Дальше React Query сделает всё сам.
Либо можно поступить по-другому и не помечать данные как устаревшие, а менять данные в кэше напрямую, чтобы не вызывать запрос. Или даже можно не ждать выполнения мутации, а оптимистично менять данные в кэше в onMutate, а при ошибке откатывать свои изменения внутри onError.
Как видно, мутации не требуют обязательного указания ключа mutationKey. Однако можно указать ключ, если в дальнейшем мы захотим получить кэш мутации по ключу.
Затем хук useMutation используется прямо в компоненте. В целом, поля, которые возвращает хук очень похожи на возвращаемые значения в useQuery:
isPending – состояние, когда мутация выполняется;
isIdle – состояние, когда мутация не выполняется;
isError – состояние ошибки;
isPaused – состояние приостановки;
isSuccess – состояние успеха;
error – объект ошибки или null;
reset – функция, которая сбрасывает мутацию до её исходного состояния;
mutate – функция вызова мутации;
mutateAsync – то же, что и mutate, но асинхронная;
data – последние успешно полученные данные по этой мутации.
const EditUserName = () => { |
То есть React Query не выполняет мутации автоматически, как запросы, поэтому их нужно вызывать вручную через mutate, куда передаются аргументы.
Итак, когда мутация выполняется, данные, которые возвращаются из запроса, сохраняются в кэше мутаций на gcTime. Но, такие данные не используются напрямую при повторной мутации, а, скорее, нужны для хранения состояния мутации.
Отладка
Плюсом библиотеки является и то, что есть официальный инструмент для визуализации работы React Query.
npm install @tanstack/react-query-devtools |
import { QueryClient, QueryClientProvider, ReactQueryDevtools } from '@tanstack/react-query'; |

Можно увидеть, какие данные запросов / мутаций хранятся в кэше, сколько в целом сейчас свежих или устаревших данных, сколько запросов выполняются, на паузе или неактивны.
По каждому запросу можно посмотреть данные в кэше, сколько времени они будут храниться, настройки этого запроса. Даже есть возможность вручную инвалидировать любой запрос, выполнить перезапуск, удалить из кэша, включить визуальную загрузку или вызвать ошибку.
Итог
React Query – это, можно сказать, библиотека, которая используется для синхронизации данных с сервером. Она автоматически перезапрашивает данные после ошибки или когда они устареют. Эти данные кэшируются и инвалидируются, когда захочет разработчик. С помощью библиотеки можно довольно просто выполнить запросы параллельно или реализовать бесконечную ленту с подзагрузкой.
И хоть, при желании React Query может частично заменить глобальный стор, всё-таки эта библиотека специализирована на управлении асинхронными операциями. Так же и наоборот, Redux, MobX и другие стейт-менеджеры тоже могут реализовать подобный функционал асинхронных операций, но результат будет либо неэффективным, либо затратным по ресурсам.