React Query 也可以与 React 的 Suspense for Data Fetching API 一起使用。为此,我们有专门的钩子:
使用 suspense 模式时,不需要 status 状态和 error 对象,而是通过使用 React.Suspense 组件(包括使用 fallback 属性和 React 错误边界来捕获错误)来替换它们。请阅读重置错误边界并查看Suspense 示例以获取有关如何设置 suspense 模式的更多信息。
如果您希望变更将错误传播到最近的错误边界(类似于查询),也可以将 throwOnError 选项设置为 true。
为查询启用 suspense 模式:
import { useSuspenseQuery } from '@tanstack/react-query'
const { data } = useSuspenseQuery({ queryKey, queryFn })
import { useSuspenseQuery } from '@tanstack/react-query'
const { data } = useSuspenseQuery({ queryKey, queryFn })
这在 TypeScript 中效果很好,因为 data 保证已定义(因为错误和加载状态由 Suspense 和 ErrorBoundaries 处理)。
另一方面,因此您不能有条件地启用/禁用查询。对于依赖查询,这通常不是必需的,因为使用 suspense,组件内部的所有查询都是串行获取的。
此查询也没有 placeholderData。要防止 UI 在更新期间被回退替换,请将更改 QueryKey 的更新包装在 startTransition 中。
并非所有错误都会默认抛出到最近的错误边界——我们仅在没有其他数据显示时才抛出错误。这意味着如果查询曾经在缓存中成功获取数据,即使数据已 stale,组件也会渲染。因此,throwOnError 的默认值为:
throwOnError: (error, query) => typeof query.state.data === 'undefined'
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
}
// 继续渲染数据
import { useSuspenseQuery } from '@tanstack/react-query'
const { data, error, isFetching } = useSuspenseQuery({ queryKey, queryFn })
if (error && !isFetching) {
throw error
}
// 继续渲染数据
无论您是在查询中使用 suspense 还是 throwOnError,当在发生某些错误后重新渲染时,您都需要一种方法来让查询知道您想重试。
可以使用 QueryErrorResetBoundary 组件或 useQueryErrorResetBoundary 钩子来重置查询错误。
使用组件时,它将重置组件边界内的任何查询错误:
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>
)
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>
)
使用钩子时,它将重置最近的 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>
)
}
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>
)
}
开箱即用,suspense 模式下的 React Query 作为 Fetch-on-render 解决方案效果非常好,无需额外配置。这意味着当您的组件尝试挂载时,它们将触发查询获取并暂停,但仅在您导入并挂载它们之后。如果您想更进一步并实现 Render-as-you-fetch 模型,我们建议在路由回调和/或用户交互事件上实现预取,以便在挂载查询之前开始加载查询,甚至希望在开始导入或挂载其父组件之前。
如果您正在使用 NextJs,可以使用我们用于服务器上 Suspense 的实验性集成:@tanstack/react-query-next-experimental。此包将允许您通过在组件中调用 useSuspenseQuery 来在服务器上(在客户端组件中)获取数据。然后,当 SuspenseBoundaries 解析时,结果将从服务器流式传输到客户端。
为此,请将您的应用程序包装在 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,我们通常希望设置一些默认的 staleTime
// 大于 0 以避免在客户端立即重新获取
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>
)
}
// 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,我们通常希望设置一些默认的 staleTime
// 大于 0 以避免在客户端立即重新获取
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 Streaming Example 和高级渲染和水合指南。
要启用此功能,您需要在创建 QueryClient 时将 experimental_prefetchInRender 选项设置为 true
示例代码:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
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>
</>
)
}
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 示例。