React Query 也可以与 React 的数据获取 Suspense API 一起使用。为此,我们提供了专门的 Hook。
在使用 Suspense 模式时,不再需要 status 状态和 error 对象,它们被 React.Suspense 组件的使用(包括 fallback prop 的使用以及用于捕获错误的 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
}
// continue rendering data
import { useSuspenseQuery } from '@tanstack/react-query'
const { data, error, isFetching } = useSuspenseQuery({ queryKey, queryFn })
if (error && !isFetching) {
throw error
}
// continue rendering data
无论您在查询中是使用 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>
There was an error!
<Button onClick={() => resetErrorBoundary()}>Try again</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>
There was an error!
<Button onClick={() => resetErrorBoundary()}>Try again</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>
There was an error!
<Button onClick={() => resetErrorBoundary()}>Try again</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>
There was an error!
<Button onClick={() => resetErrorBoundary()}>Try again</Button>
</div>
)}
>
<Page />
</ErrorBoundary>
)
}
开箱即用,React Query 在 suspense 模式下作为一个 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: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 60 * 1000,
},
},
})
}
let browserQueryClient: QueryClient | undefined = undefined
function getQueryClient() {
if (isServer) {
// Server: always make a new query client
return makeQueryClient()
} else {
// Browser: make a new query client if we don't already have one
// This is very important, so we don't re-make a new client if React
// suspends during the initial render. This may not be needed if we
// have a suspense boundary BELOW the creation of the query client
if (!browserQueryClient) browserQueryClient = makeQueryClient()
return browserQueryClient
}
}
export function Providers(props: { children: React.ReactNode }) {
// NOTE: Avoid useState when initializing the query client if you don't
// have a suspense boundary between this and the code that may
// suspend because React will throw away the client on the initial
// render if it suspends and there is no boundary
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: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 60 * 1000,
},
},
})
}
let browserQueryClient: QueryClient | undefined = undefined
function getQueryClient() {
if (isServer) {
// Server: always make a new query client
return makeQueryClient()
} else {
// Browser: make a new query client if we don't already have one
// This is very important, so we don't re-make a new client if React
// suspends during the initial render. This may not be needed if we
// have a suspense boundary BELOW the creation of the query client
if (!browserQueryClient) browserQueryClient = makeQueryClient()
return browserQueryClient
}
}
export function Providers(props: { children: React.ReactNode }) {
// NOTE: Avoid useState when initializing the query client if you don't
// have a suspense boundary between this and the code that may
// suspend because React will throw away the client on the initial
// render if it suspends and there is no boundary
const queryClient = getQueryClient()
return (
<QueryClientProvider client={queryClient}>
<ReactQueryStreamedHydration>
{props.children}
</ReactQueryStreamedHydration>
</QueryClientProvider>
)
}
有关更多信息,请查看NextJs Suspense Streaming 示例和高级渲染与 Hydration指南。
要启用此功能,您需要在创建 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>Todos</h1>
<React.Suspense fallback={<div>Loading...</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>Todos</h1>
<React.Suspense fallback={<div>Loading...</div>}>
<TodoList query={query} />
</React.Suspense>
</>
)
}
有关更完整的示例,请参阅 GitHub 上的 Suspense 示例。
有关 Next.js 流式传输示例,请参阅 GitHub 上的 nextjs-suspense-streaming 示例。