Доки по разработке
This project is maintained by teniryte
TanStack Query v5 тесно интегрируется с React 18 Suspense и Error Boundaries, позволяя строить декларативные экраны загрузки/ошибок. Ниже собраны практики, которые обычно остаются за кадром в базовых примерах.
useSuspenseQueryimport { useSuspenseQuery } from '@tanstack/react-query';
const Article = ({ slug }: { slug: string }) => {
const { data } = useSuspenseQuery(articleQuery(slug));
return <ArticleBody article={data} />;
};
export const ArticlePage = ({ slug }: { slug: string }) => (
<ErrorBoundary fallback={<ErrorState />}>
<Suspense fallback={<ArticleSkeleton />}>
<Article slug={slug} />
</Suspense>
</ErrorBoundary>
);
Promise/Error, когда данные не готовы — это перехватывается ближайшим Suspense/ErrorBoundary.Возвращает только { data } (без статусных флагов). Для расширенных данных используйте useQuery + suspense: true.
throwOnError: true и временно сбрасывайте кэш через queryClient.invalidateQueries.useSuspenseQuery потребляет опции enabled, select, placeholderData, retry. retry по умолчанию продолжает выполняться до тех пор, пока Suspense не перестанет кидать Promise, поэтому при длинных или нестабильных запросах имеет смысл ограничить retry: 1 и добавить кнопку ручного повторного запроса.isFetching, errorUpdatedAt), комбинируйте useSuspenseQuery с useQueryClient().getQueryState(queryKey) и подпиской через queryClient.getQueryData.suspense: true в useQueryconst { data } = useQuery({
...profileQuery(id),
suspense: true,
useErrorBoundary: true,
});
suspense: true заставляет useQuery вести себя как useSuspenseQuery.select, placeholderData, enabled.networkMode: 'always' заставит запрос продолжать работу даже при оффлайне и Suspense будет показывать offline fallback. Полезно для “ожидаемого” ожидания, например, загрузки больших файлов.retryOnMount: false вместе с suspense: true предотвращает повторную суспензию при ремоунте, если данные всё ещё недоступны (актуально для client-side routing, когда пользователь возвращается на страницу назад).throwOnError. Если вернуть функцию, можно тонко решить, какие ошибки идут через Error Boundary, а какие показываются локально:
useQuery({
...paymentIntentQuery(),
suspense: true,
throwOnError: error => error.code !== 'OTP_REQUIRED',
});
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => (
<RetryView onRetry={resetErrorBoundary} />
)}>
<Suspense fallback={<Spinner />}>
<Todos />
</Suspense>
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
QueryErrorResetBoundary предоставляет reset, который сбрасывает состояние ошибки всех дочерних запросов.ErrorBoundary вызывайте resetErrorBoundary, чтобы повторно попытаться загрузить данные.useErrorBoundary: (error) => Boolean(error?.response?.status === 500) полезно, когда часть ошибок стоит обрабатывать inline, а часть пробрасывать наружу.queryClient.resetQueries({ queryKey, exact: true }) внутри onReset.select выполняется до того, как Suspense снимет ожидание, поэтому можно безопасно маппить данные:
const { data: todoTitles } = useSuspenseQuery({
...todosQuery(),
select: todos => todos.map(t => t.title),
});
Suspense запросыconst { data: user } = useSuspenseQuery(userQuery(id));
const { data: projects } = useSuspenseQuery({
...projectsQuery(user.teamId),
enabled: Boolean(user.teamId),
});
Если enabled: false, Suspense не будет ждать запрос (он просто не запустится).
placeholderData. Оно позволяет отрендерить минимальную форму интерфейса и избежать лишнего мигания:
const { data: projects } = useSuspenseQuery({
...projectsQuery(user.teamId),
enabled: Boolean(user.teamId),
placeholderData: keepPreviousData,
});
queryKey, используйте structuralSharing (по умолчанию включён) чтобы реакт не делал полный re-render.Suspense<Sidebar />) в отдельный Suspense, чтобы не блокировать основной контент.SuspenseList помогает контролировать порядок появления секций: revealOrder="forwards" показывает элементы по мере готовности, сохраняя UX.React.lazy(() => import('./Chart')) + Suspense, а данные подгружайте через useSuspenseQuery внутри ленивого компонента.suspenseSuspense. В сочетании с dehydrate можно выдавать частично готовые страницы.use server/use client разделение + <Suspense>.await queryClient.prefetchQuery() перед dehydrate. В противном случае сервер не знает, что нужно ждать, и вернёт fallback ещё до данных.serverPrefetch (в Remix/Next loaders) чтобы не дожидаться Suspense на клиенте. Клиентская граница всё равно нужна для последующих переходов.useErrorBoundary может быть функцией: useErrorBoundary: (error) => error.status === 404.status === 'error') без Error Boundary.AppShell, но при этом ставить локальные границы внутри критичных участков (например, список задач). Это позволяет падать только части дерева.ErrorBoundary с startTransition, если после восстановления вы переводите пользователя на другой маршрут:
const handleRetry = () => {
startTransition(() => {
resetErrorBoundary();
navigate('/dashboard');
});
};
S).select ошибку или не завис ли queryFn.Show Offline, чтобы проверить, как ведёт себя Suspense при window.navigator.onLine = false.renderWithClient + await waitFor(() => screen.getByText(...)): Suspense внутри React Testing Library требует оборачивания тестируемого компонента в Suspense и ErrorBoundary.useSuspenseInfiniteQueryconst {
data,
fetchNextPage,
hasNextPage,
} = useSuspenseInfiniteQuery({
queryKey: ['feed'],
queryFn: ({ pageParam = 0 }) => fetchFeed(pageParam),
getNextPageParam: lastPage => lastPage.nextCursor,
});
<Suspense fallback={<InlineSpinner />}>, чтобы при загрузке следующей пачки не блокировать весь список.staleTime — главный инструмент управления тем, насколько часто Suspense будет срабатывать. Если значение высокое, то при фокусе вкладки пользователь не увидит повторный fallback.placeholderData: previousData + suspense: true предотвращает пустой экран при смене параметров пагинации.queryClient.ensureQueryData(queryOptions) в эффектах или роутерах, чтобы подогревать кэш до маунта компонента. Тогда Suspense вообще не понадобится.defer и Await. TanStack Query всё ещё полезен: loader делает await queryClient.ensureQueryData, а в компоненте вы спокойно читаете useSuspenseQuery и получаете синхронный доступ.useSuspenseQuery работает только в клиентских компонентов ('use client'). Для серверных частей используйте queryClient.fetchQuery и передавайте данные пропсами.queryClient.invalidateQueries до того, как компонент успеет посетить Suspense, если хотите пересуспендить. Либо используйте setQueryData для мгновенного обновления UI.rollbackOnError, иначе Error Boundary покажет ошибку, но список останется в оптимистичном состоянии.resetKeys. Иначе повторный заход на страницу с тем же queryKey может сразу падать.queryFn и редиректить.suspense: false и проверяйте status в компоненте. После фикса возвращайте обратно.SuspenseBoundary компонент, в котором будут заложены общие fallback, логирование и счётчики (Sentry, Datadog).