框架
版本

Suspense

React Query 也可以与 React 的 Suspense for Data Fetching API 一起使用。为此,我们有专门的钩子:

使用 suspense 模式时,不需要 status 状态和 error 对象,而是通过使用 React.Suspense 组件(包括使用 fallback 属性和 React 错误边界来捕获错误)来替换它们。请阅读重置错误边界并查看Suspense 示例以获取有关如何设置 suspense 模式的更多信息。

如果您希望变更将错误传播到最近的错误边界(类似于查询),也可以将 throwOnError 选项设置为 true

为查询启用 suspense 模式:

tsx
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 中。

throwOnError 默认值

并非所有错误都会默认抛出到最近的错误边界——我们仅在没有其他数据显示时才抛出错误。这意味着如果查询曾经在缓存中成功获取数据,即使数据已 stale,组件也会渲染。因此,throwOnError 的默认值为:

throwOnError: (error, query) => typeof query.state.data === 'undefined'
throwOnError: (error, query) => typeof query.state.data === 'undefined'

由于您无法更改 throwOnError(因为它会导致 data 可能变为 undefined),因此如果您希望所有错误都由错误边界处理,则必须手动抛出错误:

tsx
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 钩子来重置查询错误。

使用组件时,它将重置组件边界内的任何查询错误:

tsx
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 内的任何查询错误。如果未定义边界,它将全局重置它们:

tsx
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>
  )
}

Fetch-on-render 与 Render-as-you-fetch

开箱即用,suspense 模式下的 React Query 作为 Fetch-on-render 解决方案效果非常好,无需额外配置。这意味着当您的组件尝试挂载时,它们将触发查询获取并暂停,但仅在您导入并挂载它们之后。如果您想更进一步并实现 Render-as-you-fetch 模型,我们建议在路由回调和/或用户交互事件上实现预取,以便在挂载查询之前开始加载查询,甚至希望在开始导入或挂载其父组件之前。

服务器上的 Suspense 与流式传输

如果您正在使用 NextJs,可以使用我们用于服务器上 Suspense 的实验性集成:@tanstack/react-query-next-experimental。此包将允许您通过在组件中调用 useSuspenseQuery 来在服务器上(在客户端组件中)获取数据。然后,当 SuspenseBoundaries 解析时,结果将从服务器流式传输到客户端。

为此,请将您的应用程序包装在 ReactQueryStreamedHydration 组件中:

tsx
// 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高级渲染和水合指南。

使用 useQuery().promiseReact.use() (实验性)

要启用此功能,您需要在创建 QueryClient 时将 experimental_prefetchInRender 选项设置为 true

示例代码:

tsx
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      experimental_prefetchInRender: true,
    },
  },
})
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      experimental_prefetchInRender: true,
    },
  },
})

用法:

tsx
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 示例