Chapter 07

请求状态与错误处理

配置重试策略、集中处理错误、用错误边界与 Suspense 构建健壮的 loading / error UI。

7.1 错误处理是生产代码的分水岭

Demo 里"能拿到数据"就行,但生产环境要面对:网络抖动、超时、401 过期、500 服务器错误。TanStack Query 提供了从"自动重试"到"全局兜底"再到"错误边界"的完整链路。

7.2 核心概念

retry
失败后自动重试次数,查询默认 3,mutation 默认 0。可传数字、false、或 (failureCount, error) => boolean 函数做条件重试(如 401 不重试)。
retryDelay
两次重试的间隔,默认指数退避(1s、2s、4s…上限 30s)。可自定义函数控制退避曲线。
throwOnError
为 true 时把查询错误抛给最近的 React Error Boundary,而非用 isError 就地处理。可传函数按错误类型决定(如仅 5xx 抛给边界)。旧名 useErrorBoundary。
Error Boundary (错误边界)
React 捕获子树渲染错误的组件。配合 throwOnError,让查询错误走统一的错误 UI,而不必每个组件写 if (isError)。
useQueryErrorResetBoundary
提供 reset 函数,让错误边界的"重试"按钮能重置查询错误状态并重新获取——把边界和 query 的错误联动起来。
QueryCache onError
在 QueryClient 上配置的全局错误回调,任何查询失败都会触发。适合统一弹 toast、上报监控、处理 401 登出。
useSuspenseQuery
Suspense 版查询 Hook:加载时抛 Promise 给 <Suspense> 显示 fallback,出错抛给 Error Boundary。data 永不为 undefined,组件里无需再判空。

7.3 配置重试策略

useQuery({
  queryKey: ["todos"],
  queryFn: fetchTodos,
  retry: (failureCount, error) => {
    // 4xx 不重试(客户端错误重试也没用)
    if (error instanceof HttpError && error.status < 500) return false;
    return failureCount < 3;
  },
  retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30_000),
});

7.4 就地错误处理(最常见)

function Todos() {
  const { data, isLoading, isError, error, refetch } = useQuery({
    queryKey: ["todos"],
    queryFn: fetchTodos,
  });

  if (isLoading) return <Skeleton />;
  if (isError) return (
    <div role="alert">
      <p>加载失败:{error.message}</p>
      <button onClick={() => refetch()}>重试</button>
    </div>
  );
  return <List items={data} />;
}

7.5 全局错误兜底

import { QueryClient, QueryCache, MutationCache } from "@tanstack/react-query";

const queryClient = new QueryClient({
  queryCache: new QueryCache({
    onError: (error, query) => {
      // 只在没有本地错误 UI 时才全局提示(可用 meta 标记)
      if (query.meta?.errorMessage) {
        toast.error(query.meta.errorMessage as string);
      }
      if ((error as HttpError).status === 401) redirectToLogin();
    },
  }),
  mutationCache: new MutationCache({
    onError: (err) => reportToSentry(err),
  }),
});
用 meta 传递上下文

每个查询可配 meta: { errorMessage: "加载订单失败" },全局 onError 里读取它给出针对性提示,既集中又保留个性化——避免所有错误都弹一样的通用文案。

7.6 错误边界 + 重置

import { useQueryErrorResetBoundary } from "@tanstack/react-query";
import { ErrorBoundary } from "react-error-boundary";

function Page() {
  const { reset } = useQueryErrorResetBoundary();
  return (
    <ErrorBoundary
      onReset={reset}
      fallbackRender={({ resetErrorBoundary }) => (
        <div>
          <p>出错了</p>
          <button onClick={resetErrorBoundary}>重试</button>
        </div>
      )}
    >
      <Todos />   {/* 内部用 throwOnError 或 useSuspenseQuery */}
    </ErrorBoundary>
  );
}

7.7 Suspense 集成

import { useSuspenseQuery } from "@tanstack/react-query";

function Profile({ id }: { id: number }) {
  // data 一定有值,无需 isLoading/undefined 判断
  const { data } = useSuspenseQuery({
    queryKey: ["user", id],
    queryFn: () => fetchUser(id),
  });
  return <h1>{data.name}</h1>;
}

// 父组件用 Suspense + ErrorBoundary 统一处理 loading/error
<ErrorBoundary fallback={<Err />}>
  <Suspense fallback={<Skeleton />}>
    <Profile id={1} />
  </Suspense>
</ErrorBoundary>
Suspense 的取舍

useSuspenseQuery 让组件代码超干净(无判空),但 loading/error 交给上层边界,粒度较粗;且它不支持 enabled、placeholderData 等选项。数据流复杂时仍建议用普通 useQuery 就地处理。

7.8 三种错误处理方式对比

方式粒度适用
isError 就地处理单组件需要局部错误 UI、可重试
throwOnError + 边界子树整块区域统一错误页
QueryCache onError全局toast、监控上报、401 登出

7.9 小结与下一步