Suspense
React Query 也可以与 React 的 Suspense 数据获取 API 一起使用。为此,我们提供了专用的 Hook:
useSuspenseQuery
useSuspenseInfiniteQuery
useSuspenseQueries
此外,你还可以使用
useQuery().promise和React.use()(实验性功能)
使用 Suspense 模式时,不再需要 status 状态和 error 对象,而是改用 React.Suspense 组件(包括使用 fallback 属性以及 React 错误边界来捕获错误)。请阅读 重置错误边界 并查看 Suspense 示例,了解如何设置 Suspense 模式。
如果你想让 mutation 也能像 query 一样将错误传播到最近的错误边界,可以将 throwOnError 选项设置为 true。
为查询启用 Suspense 模式:
import { useSuspenseQuery } from '@tanstack/react-query'
const { data } = useSuspenseQuery({ queryKey, queryFn })在 TypeScript 中,这种方式非常友好,因为 data 是有保证的(加载状态和错误由 Suspense 和 ErrorBoundary 处理)。
但反过来说,你因此无法条件性地启用或禁用查询。对于依赖查询,这通常不是问题,因为使用 Suspense 时,组件内的所有查询会串行执行。
此查询也没有 placeholderData。为了防止在更新时 UI 被 fallback 替换,应将更改 QueryKey 的更新操作包裹在 startTransition 中。
throwOnError 默认行为
默认情况下,并非所有错误都会抛出到最近的错误边界——我们只在没有其他数据可显示时才抛出错误。这意味着,如果查询曾经成功获取过缓存数据,即使数据已过期(stale),组件也会渲染。因此,throwOnError 的默认值为:
throwOnError: (error, query) => typeof query.state.data === 'undefined'由于你无法更改 throwOnError(因为这可能导致 data 变为 undefined),如果你想让所有错误都由错误边界处理,则需要手动抛出错误:
import { useSuspenseQuery } from '@tanstack/react-query'
const { data, error, isFetching } = useSuspenseQuery({ queryKey, queryFn })
if (error && !isFetching) {
throw error
}
// 继续渲染数据重置错误边界
无论你是在查询中使用 suspense 还是 throwOnError,当某个错误发生后重新渲染时,你都需要一种方式来告知查询:你想重试。
可以使用 QueryErrorResetBoundary 组件或 useQueryErrorResetBoundary Hook 来重置查询错误。
使用组件时,它会重置其组件边界内的任何查询错误:
import { QueryErrorResetBoundary } from '@tanstack/react-query'
import { ErrorBoundary } from 'react-error-boundary'
const App = () => (
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
onReset={reset}
fallbackRender={({ resetErrorBoundary }) => (
<div>
出现了错误!
<Button onClick={() => resetErrorBoundary()}>重试</Button>
</div>
)}
>
<Page />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
)使用 Hook 时,它会重置最近的 QueryErrorResetBoundary 内的任何查询错误。如果没有定义边界,则会全局重置:
import { useQueryErrorResetBoundary } from '@tanstack/react-query'
import { ErrorBoundary } from 'react-error-boundary'
const App = () => {
const { reset } = useQueryErrorResetBoundary()
return (
<ErrorBoundary
onReset={reset}
fallbackRender={({ resetErrorBoundary }) => (
<div>
出现了错误!
<Button onClick={() => resetErrorBoundary()}>重试</Button>
</div>
)}
>
<Page />
</ErrorBoundary>
)
}渲染时获取 vs 边渲染边获取
开箱即用,React Query 的 suspense 模式可作为 渲染时获取 (Fetch-on-render) 的优秀解决方案,无需额外配置。这意味着当你的组件尝试挂载时,会触发查询并挂起,但仅在你导入并挂载它们时才发生。如果你想更进一步,实现 边渲染边获取 (Render-as-you-fetch) 模型,我们建议在路由回调和/或用户交互事件中实现 预取 (Prefetching),以便在组件挂载前就开始加载查询,甚至在你开始导入或挂载其父组件之前。
服务端的 Suspense 与流式传输
如果你使用的是 NextJs,可以使用我们的 实验性 集成包 @tanstack/react-query-next-experimental 来在服务端使用 Suspense。该包允许你在客户端组件中通过直接调用 useSuspenseQuery 在服务端获取数据。结果将在 Suspense 边界解析时从服务端流式传输到客户端。
要实现此功能,请用 ReactQueryStreamedHydration 组件包裹你的应用:
// app/providers.tsx
'use client'
import {
isServer,
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query'
import * as React from 'react'
import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental'
function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
// 使用 SSR 时,通常希望设置一个大于 0 的默认 staleTime
// 以避免在客户端立即重新获取
staleTime: 60 * 1000,
},
},
})
}
let browserQueryClient: QueryClient | undefined = undefined
function getQueryClient() {
if (isServer) {
// 服务端:始终创建新的查询客户端
return makeQueryClient()
} else {
// 浏览器:如果没有则创建新的查询客户端
// 这一点非常重要,可以避免 React 在初始渲染挂起时丢弃客户端
// 如果在创建查询客户端下方有 suspense 边界,可能就不需要这样
if (!browserQueryClient) browserQueryClient = makeQueryClient()
return browserQueryClient
}
}
export function Providers(props: { children: React.ReactNode }) {
// 注意:如果在此处和可能挂起的代码之间没有 suspense 边界,
// 则避免在初始化查询客户端时使用 useState,因为 React 可能会在
// 初始渲染挂起时丢弃客户端
const queryClient = getQueryClient()
return (
<QueryClientProvider client={queryClient}>
<ReactQueryStreamedHydration>
{props.children}
</ReactQueryStreamedHydration>
</QueryClientProvider>
)
}更多信息,请查看 NextJs Suspense 流式传输示例 和 高级渲染与水合 指南。
使用 useQuery().promise 和 React.use()(实验性功能)
useQuery().promise 和 React.use()(实验性功能)要启用此功能,需要在创建
QueryClient时将experimental_prefetchInRender选项设置为true
示例代码:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
experimental_prefetchInRender: true,
},
},
})使用方法:
import React from 'react'
import { useQuery } from '@tanstack/react-query'
import { fetchTodos, type Todo } from './api'
function TodoList({ query }: { query: UseQueryResult<Todo[]> }) {
const data = React.use(query.promise)
return (
<ul>
{data.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)
}
export function App() {
const query = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
return (
<>
<h1>待办事项</h1>
<React.Suspense fallback={<div>加载中...</div>}>
<TodoList query={query} />
</React.Suspense>
</>
)
}更多完整示例,请参见 GitHub 上的 suspense 示例。
Next.js 流式传输示例,请参见 GitHub 上的 nextjs-suspense-streaming 示例。
最后更新于
这有帮助吗?