React Query 也可以与 React 的 Suspense for Data Fetching APIs 一起使用。为此,我们提供了专用的 hooks
当使用 suspense 模式时,status 状态和 error 对象是不需要的,然后被 React.Suspense 组件(包括使用 fallback prop 和用于捕获错误的 React 错误边界)的使用所取代。请阅读 重置错误边界 并查看 Suspense 示例 以获取有关如何设置 suspense 模式的更多信息。
如果您希望 mutations 将错误传播到最近的错误边界(类似于 queries),您可以将 throwOnError 选项设置为 true。
为 query 启用 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 处理)。
另一方面,因此您不能有条件地启用/禁用 Query。对于依赖 Queries,这通常是不必要的,因为使用 suspense,您在一个组件内的所有 Queries 都是串行获取的。
placeholderData 也不存在于此 Query。为了防止 UI 在更新期间被 fallback 替换,请将更改 QueryKey 的更新包装到 startTransition 中。
并非所有错误都默认抛给最近的 Error Boundary - 只有在没有其他数据可显示时,我们才会抛出错误。这意味着如果 Query 曾经在缓存中成功获取过数据,即使数据是 stale,组件也将渲染。因此,throwOnError 的默认值为
throwOnError: (error, query) => typeof query.state.data === 'undefined'
throwOnError: (error, query) => typeof query.state.data === 'undefined'
由于您无法更改 throwOnError(因为它会允许 data 变得潜在地 undefined),如果您希望所有错误都由 Error Boundaries 处理,则必须手动抛出错误
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
无论您在 queries 中使用 suspense 还是 throwOnError,您都需要一种方法让 queries 知道您希望在发生某些错误后重新渲染时再次尝试。
可以使用 QueryErrorResetBoundary 组件或 useQueryErrorResetBoundary hook 重置 Query 错误。
当使用组件时,它将重置组件边界内的任何 query 错误
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 内的任何 query 错误。 如果没有定义边界,它将全局重置它们
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>
)
}
开箱即用,suspense 模式下的 React Query 作为 Fetch-on-render 解决方案非常好,无需额外配置。 这意味着当您的组件尝试挂载时,它们将触发 query 获取并暂停,但仅在您导入并挂载它们之后。 如果您想将其提升到一个新的水平并实现 Render-as-you-fetch 模型,我们建议在路由回调和/或用户交互事件上实现 预取,以便在 queries 挂载之前甚至在您开始导入或挂载其父组件之前开始加载。
如果您正在使用 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 流式处理示例 和 高级渲染 & 水合 指南。
要启用此功能,您需要在创建 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>
</>
)
}