React Query — одна из самых популярных библиотек для управления серверным состоянием в экосистеме React. По статистике npmtrends, React Query используется почти в каждом 5 приложении на React. Но что сделало её такой популярной?
Главное её достоинство (со слов одного из соавторов) заключается в том, что всего одна функция в её составе предлагает 80% всей функциональности. Речь, конечно же, о useQuery
, которая включает в себя: глобальное управление состоянием, дедупликацию запросов, кэширование, повторные запросы, управление состояниями загрузки и ошибки, запросы при получении фокуса окном и многое другое. В рамках этой статьи хотелось бы особенно заострить внимание на управлении состояниями загрузки и ошибок, потому что до недавних пор оно часто вызывало негодование лично у меня.
Проблема с useQuery
Рассмотрим простой пример с useQuery
:
function MyComponent() {
const { data, isLoading, isError } = useQuery({
queryKey: ['data'],
queryFn: fetchData,
});
if (isLoading) return <div>Загрузка...</div>;
if (isError) return <div>Ошибка!</div>;
return <div>{data}</div>;
}
На первый взгляд кажется, что это довольно элегантный код, за который все и полюбили React Query: мы получаем состояния загрузки/ошибок (isLoading
/isError
) и в зависимости от их значений выводим информацию для пользователя до тех пор, пока серверное состояние не будет определено (data !== undefined
).
Но это код лишь одного компонента. Наряду с ним, на страницу могут быть встроены и другие подобные компонентами, использующие useQuery
. Тот факт, что каждый компонент сам отвечает за своё состояние загрузки означает, что в определённый момент страница будет состоять из кучи "загрузок" (похожая ситуация и с ошибками). Так себе UX, не правда ли?
Чтобы решить эту проблему, приходилось прибегать к довольно странным методам. Можно было вынести загрузку данных на уровень страницы, а сами данные прокидывать соответствующим компонентам, либо оборачивать такие компоненты в какой-то другой компонент (во избежание дублирования кода), который бы выполнял загрузку и так или иначе влиял бы на состояние загрузки/ошибки у страницы целиком. Как бы там ни было, качество кода страдало. Но с появлением useSuspenseQuery
вышеупомянутая проблема ушла в прошлое.
Решение с useSuspenseQuery
useSuspenseQuery
— это относительно новый хук в React Query v5, который позволяет использовать Suspense
(встроенный компонент React, который приостанавливает отрисовку компонентов до выполнения определенного условия и отображает "заглушку" — fallback) для рендеринга компонентов во время получения данных. В отличие от useQuery
, где вам нужно вручную проверять состояния isLoading
, isError
и data
, с useSuspenseQuery
вы можете быть уверены, что данные (data
) всегда будут определены, когда компонент рендерится. Это упрощает код, так как не нужно писать условные проверки. В остальном, за некоторыми исключениями, он работает так же, как и useQuery
.
Пример с useSuspenseQuery
:
function MyComponent() {
const { data } = useSuspenseQuery({
queryKey: ['data'],
queryFn: fetchData,
});
return <div>{data}</div>; // data всегда не undefined
}
function MyPage() {
return <ErrorBoundary>
<Suspense fallback={<div>Загрузка...</div>}>
<MyComponent />
</Suspense>
</ErrorBoundary>
}
С useSuspenseQuery
не нужно вручную обрабатывать состояния загрузки (isLoading
) и ошибки (isError
) внутри компонента. Эти состояния делегируются Suspense
и ErrorBoundary
, что делает код чище и более декларативным. Это особенно полезно в больших приложениях, где управление состояниями в каждом компоненте может привести к дублированию кода. Использование Suspense
позволяет показывать "заглушки" (fallbacks) на уровне всего дерева компонентов, а не в каждом отдельном месте, где происходит загрузка данных. Например, если у вас есть несколько компонентов, использующих useSuspenseQuery
, вы можете обернуть их в один Suspense
с единым индикатором загрузки, вместо того чтобы отображать несколько отдельных спиннеров.
В useSuspenseQuery
свойство data
типизировано как TData
(без undefined
), в отличие от useQuery
, где data
имеет тип TData | undefined
. Это исключает необходимость проверки на undefined
.
Ограничения
Хотя useSuspenseQuery
имеет преимущества, он не всегда превосходит useQuery
. Есть ситуации, где useQuery
может быть предпочтительнее:
- Отсутствие опции
enabled
: в useSuspenseQuery
нельзя отключить запрос с помощью параметра enabled
, как в useQuery
. Это ограничивает его использование в случаях, когда запрос должен быть условным.
- Нет
placeholderData
или keepPreviousData
: эти опции, доступные в useQuery
, позволяют отображать промежуточные данные или сохранять предыдущие данные при повторных запросах. В useSuspenseQuery
таких возможностей нет, так как он полагается на Suspense
для управления состояниями.
- Требуется настройка
Suspense
и ErrorBoundary
: для использования useSuspenseQuery
нужно настроить Suspense
и обработку ошибок, что может добавить сложности в небольших проектах.
Вывод
useSuspenseQuery
превосходит useQuery
в сценариях, где важны простота кода, интеграция с Suspense
и улучшенный UX за счет делегирования управления состояниями загрузки/ошибки. Однако, он требует определённого подхода к архитектуре приложения и может быть менее гибким в некоторых случаях. Выбор между ними зависит от потребностей и структуры проекта, но лично я всё чаще отдаю предпочтение useSuspenseQuery.