null

React Query: useQuery или useSuspenseQuery?

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.