useQuery предоставляет встроенные механизмы обработки
ошибок через свойства error, isError,
status, failureCount, retry и
callbacks. Однако при росте приложения возникает проблема дублирования
логики:
const usersQuery = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
})
if (usersQuery.isError) {
return <ErrorMessage />
}
Через некоторое время аналогичные проверки появляются во множестве компонентов:
if (query.isError) {
return <ErrorPage />
}
или:
if (query.error) {
toast.error(query.error.message)
}
Подобный подход создаёт несколько проблем:
Для решения этих задач в TanStack Query существует механизм Error Boundaries.
Error Boundary — специальный React-компонент, перехватывающий ошибки внутри дерева компонентов.
Пример базовой границы ошибок:
class ErrorBoundary extends React.Component {
state = {
hasError: false,
}
static getDerivedStateFromError() {
return {
hasError: true,
}
}
render() {
if (this.state.hasError) {
return <h1>Ошибка приложения</h1>
}
return this.props.children
}
}
React Error Boundary умеет перехватывать:
Но существует важное ограничение.
useQueryОшибка запроса возникает асинхронно:
const query = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
})
fetchUsers() выполняется вне процесса рендера
компонента.
Следовательно:
isError;Именно поэтому TanStack Query предоставляет механизм проброса ошибок в React Error Boundary.
throwOnErrorДля интеграции с Error Boundary используется параметр:
throwOnError
Пример:
const usersQuery = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
throwOnError: true,
})
Теперь при ошибке TanStack Query:
Обычно структура выглядит так:
<ErrorBoundary>
<UsersPage />
</ErrorBoundary>
Компонент:
function UsersPage() {
const query = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
throwOnError: true,
})
return (
<UsersList users={query.data} />
)
}
Если запрос завершится ошибкой:
UsersPage не завершит рендер;react-error-boundaryНа практике чаще используется библиотека:
npm install react-error-boundary
Пример:
import { ErrorBoundary } from 'react-error-boundary'
function ErrorFallback({ error }) {
return (
<div>
<h2>Ошибка загрузки</h2>
<pre>{error.message}</pre>
</div>
)
}
function App() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
>
<UsersPage />
</ErrorBoundary>
)
}
Компонент запроса:
function UsersPage() {
const query = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
throwOnError: true,
})
return (
<UsersList users={query.data} />
)
}
Алгоритм работы выглядит примерно так:
queryFn()
throw new Error('Network Error')
query.state.error
throw error
<FallbackComponent />
isError от
throwOnErrorisErrorif (query.isError) {
return <ErrorMessage />
}
Особенности:
throwOnError: true
Особенности:
Границы ошибок особенно полезны для:
Пример удачного сценария:
<UserPage />
Если страница пользователя не загрузилась, проще показать целиком fallback-экран.
isError
лучше Error BoundaryНе каждая ошибка должна ломать UI.
Например:
const notificationsQuery = useQuery({
queryKey: ['notifications'],
queryFn: fetchNotifications,
})
Если уведомления не загрузились:
Пример:
if (query.isError) {
return null
}
Очень часто используется комбинация:
Пример:
const profileQuery = useQuery({
queryKey: ['profile'],
queryFn: fetchProfile,
throwOnError: true,
})
const recommendationsQuery = useQuery({
queryKey: ['recommendations'],
queryFn: fetchRecommendations,
})
Профиль критичен для страницы.
Рекомендации — нет.
throwOnErrorПараметр принимает функцию.
Пример:
throwOnError: (error) => {
return error.status >= 500
}
Теперь:
Очень распространённый паттерн:
throwOnError: (error) => {
if (error.status === 401) {
return false
}
return true
}
Здесь:
401 обрабатывается локально;401const query = useQuery({
queryKey: ['profile'],
queryFn: fetchProfile,
throwOnError: (error) => {
return error.status >= 500
},
})
if (query.error?.status === 401) {
return <LoginRequired />
}
Такой подход особенно полезен для:
По умолчанию TanStack Query повторяет запрос:
retry: 3
Важно понимать:
Error Boundary НЕ сработает до завершения всех retry.
Алгоритм:
1. Ошибка
2. Retry
3. Ошибка
4. Retry
5. Ошибка
6. Error Boundary
Иногда ошибки должны отображаться сразу:
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
retry: false,
throwOnError: true,
})
Особенно полезно для:
Агрессивный retry может ухудшать пользовательский опыт.
Например:
retry: 10
Проблемы:
Чаще используются:
retry: 1
или:
retry: false
После ошибки boundary остаётся в fallback состоянии.
Для повторной попытки нужен reset.
TanStack Query предоставляет специальный механизм:
QueryErrorResetBoundary
QueryErrorResetBoundaryПример:
import {
QueryErrorResetBoundary,
} from '@tanstack/react-query'
import { ErrorBoundary } from 'react-error-boundary'
function App() {
return (
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
onRe set={reset}
fallbackRender={({ resetErrorBoundary }) => (
<div>
<h2>Ошибка</h2>
<button
onCl ick={() => {
resetErrorBoundary()
}}
>
Повторить
</button>
</div>
)}
>
<UsersPage />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
)
}
QueryErrorResetBoundaryБез reset запрос останется в ошибочном состоянии:
status === 'error'
Даже после повторного рендера компонент снова выбросит ошибку.
reset() очищает error-state query.
После вызова:
reset()
TanStack Query:
После reset обычно происходит новый mount компонента:
<UsersPage />
И запрос запускается заново.
Error Boundary особенно хорошо сочетается с Suspense.
Пример:
<Suspense fallback={<Loader />}>
<ErrorBoundary fallback={<ErrorScreen />}>
<UsersPage />
</ErrorBoundary>
</Suspense>
В такой схеме:
Компонент становится значительно чище:
function UsersPage() {
const query = useSuspenseQuery({
queryKey: ['users'],
queryFn: fetchUsers,
})
return (
<UsersList users={query.data} />
)
}
useSuspenseQueryuseSuspenseQuery автоматически выбрасывает ошибки.
Поэтому:
throwOnError: true
обычно не требуется.
Boundary можно размещать точечно.
Пример:
<Dashboard>
<Sidebar />
<ErrorBoundary fallback={<WidgetError />}>
<AnalyticsWidget />
</ErrorBoundary>
</Dashboard>
Ошибка внутри analytics не сломает весь dashboard.
React поддерживает nested boundaries.
Пример:
<GlobalBoundary>
<PageBoundary>
<WidgetBoundary>
<Widget />
</WidgetBoundary>
</PageBoundary>
</GlobalBoundary>
TanStack Query корректно работает с такой архитектурой.
Обычно создаётся единый компонент:
function DefaultErrorFallback({ error }) {
return (
<div className="error-screen">
<h1>Что-то пошло не так</h1>
<p>{error.message}</p>
</div>
)
}
Преимущества:
Error Boundary удобно использовать вместе с:
Пример:
function ErrorFallback({ error }) {
useEffect(() => {
captureException(error)
}, [error])
return (
<ErrorScreen />
)
}
Без Error Boundary можно случайно получить множественные уведомления:
if (query.isError) {
toast.error('Ошибка')
}
Каждый рендер будет вызывать toast повторно.
Лучше использовать:
useEffect(() => {
if (query.isError) {
toast.error(query.error.message)
}
}, [query.isError])
Но при большом количестве запросов даже это становится трудно поддерживать.
Крупные приложения часто используют архитектуру:
Component
↓
useQuery
↓
Error Boundary
↓
Monitoring
↓
Logging
↓
Analytics
Так достигается:
При SSR ошибки могут возникать:
TanStack Query позволяет унифицировать обработку через Error Boundary.
Error Boundary не ловит:
Пример:
<button
onCl ick={async () => {
await dangerousAction()
}}
>
Save
</button>
Такие ошибки нужно обрабатывать отдельно через
try/catch.
Плохой пример:
<ErrorBoundary>
<EntireApplication />
</ErrorBoundary>
Проблема:
Обычно boundaries размещают:
Плохой пример:
throwOnError: true
для абсолютно всех запросов.
Например:
Это приводит к чрезмерному количеству fallback UI.
Чаще всего используется следующая стратегия:
isError
для:
throwOnError
для:
В крупных приложениях часто применяется структура:
App Boundary
├── Route Boundary
│ ├── Feature Boundary
│ │ ├── Widget Boundary
Это позволяет: