React 学习站
React›进阶›进阶

Suspense 与 Error Boundary

进阶进阶

一句话定义

<Suspense fallback> 声明「子树没准备好时显示什么」(加载中),Error Boundary 声明「子树抛错时显示什么」(降级 UI)——两者都是声明式的状态容器,把 loading/error 从业务代码中剥离出来。

为什么重要

手写 loading && <Spinner/> 分支会把每个数据组件污染成三态机器;而 Error Boundary 无法用函数组件自己实现(必须类组件)。掌握这两件工具是从「能跑的页面」到「健壮的产品」的门槛,也是理解 RSC 流式渲染(kp-024)的前置课。

前置知识

  • kp-008 提交阶段。
  • kp-019 lazy 与代码分割的动机。

核心概念

  • Suspense 边界:捕获子树抛出的「Promise 挂起」,显示 fallback;数据就绪后自动换回内容。
  • Error Boundary:捕获子树渲染期错误(含生命周期),显示降级 UI;类组件 static getDerivedStateFromError + componentDidCatch 实现(函数组件尚无一等公民写法,通常用库或类)。
  • 边界不捕获:事件处理器错误、effect 里的错误、服务端渲染中的流式错误边界规则不同、异步回调错误——只管「渲染阶段」。
  • 多个 Suspense 嵌套:内层先就绪先显示(渐进式 UI),外层兜底。
  • 来源:lazy()、支持 Suspense 的数据层(TanStack Query 的 suspense 模式、Relay、RSC 流)、use()。

原理 / 机制

三态分离的范式转变:

jsx// ❌ 手写三态:每个组件重复一遍
function Profile() {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(true);
  useEffect(() => { ... }, []);
  if (loading) return <Spinner/>;
  if (error) return <ErrorMsg/>;
  return <div>{data.name}</div>;
}

// ✅ 声明式:组件只写"成功态",容器管其余
<ErrorBoundary fallback={<ErrorMsg/>}>
  <Suspense fallback={<ProfileSkeleton/>}>
    <Profile />   // 内部用支持 Suspense 的数据源;只写成功态 JSX
  </Suspense>
</ErrorBoundary>

Error Boundary 的实现骨架(理解即可,项目里用 react-error-boundary 库):

jsxclass ErrorBoundary extends React.Component {
  state = { error: null };

  static getDerivedStateFromError(error) {
    return { error };              // 渲染降级 UI
  }
  componentDidCatch(error, info) {
    reportToSentry(error, info);   // 上报副作用
  }
  render() {
    if (this.state.error) return this.props.fallback;
    return this.props.children;
  }
}

渐进式加载(嵌套 Suspense):

jsx<Suspense fallback={<PageSkeleton/>}>
  <Header />                          {/* 秒出 */}
  <Suspense fallback={<FeedSkeleton/>}>
    <Feed />                          {/* 数据慢,先出骨架 */}
  </Suspense>
  <Suspense fallback={<AdsSkeleton/>}>
    <AdSlot />
  </Suspense>
</Suspense>

Feed 未就绪不会阻塞 Header/AdSlot——Suspense 的粒度就是你 UX 的粒度。

直观类比

Suspense 像酒店入住流程:客人(数据)没到,前台先发一张「稍后入住房卡」(fallback),客人一到自动换真房卡;每个楼层(Suspense 边界)独立办理,二层客人到了不用等五层。Error Boundary 像消防喷淋:某个房间(子树)起火(抛错),喷淋只关闭该房间并亮警示牌(fallback),其他楼层照常营业;且喷淋不负责「客人自己摔跤」(事件处理器错误)。

实例 / 案例

配合 lazy 的路由级分割(SPA 标配):

jsxconst routes = {
  '/': lazy(() => import('./pages/Home')),
  '/admin': lazy(() => import('./pages/Admin')),   // 重型管理页
  '/login': lazy(() => import('./pages/Login')),
};

function Router({ path }) {
  const Page = routes[path] ?? NotFound;
  return (
    <ErrorBoundary fallback={<p>页面出错了,请刷新</p>}>
      <Suspense fallback={<p>加载中…</p>}>
        <Page />
      </Suspense>
    </ErrorBoundary>
  );
}

kp-019 的首屏优化与 kp-027 的路由测试都以这个形状落地。

常见误区

  • 以为 Suspense 能捕获一切错误:它只处理「挂起」,错误归 Error Boundary;两者经常成对出现。
  • 在事件处理器里抛错指望 Boundary 兜底:渲染期之外的错误必须 try/catch(或交给 Actions 的错误语义,kp-025)。
  • Boundary 包太粗:整个 App 一个 Boundary → 一角出错全屏白降级;按「独立可降级的区域」划界。
  • fallback 闪烁:请求太快导致 fallback 一闪而过;用 startTransition(kp-023)或数据层的「已有内容保持显示」策略。
  • 类组件 Boundary 不会写也不想写:生产环境用 react-error-boundary 的 useErrorBoundary/<ErrorBoundary> 包装即可。

自测题

  1. 「渲染中抛出的错误」「事件处理器错误」「effect 中的错误」三者谁会被 Boundary 捕获?其余怎么处理?
  2. 写出「页面级 lazy + Suspense + ErrorBoundary」的组合结构,并说明三者职责。
  3. 嵌套 Suspense 如何实现「先出骨架的模块先显示」?
  4. 为什么说 Error Boundary 不能用函数组件自身实现(提示:它需要在渲染失败后仍能渲染,函数组件没有这个生命周期位)。
  5. fallback 闪烁的两个成因与对应解法。

与其他知识点的关系

  • 向前:lazy(kp-019)是它最常见的触发源。
  • 向后:kp-022 中 Suspense 是并发渲染的一等公民(显示 fallback 时可继续响应交互);kp-023 用 startTransition 消除闪烁;kp-024 的 RSC 流式渲染本质是服务端驱动的 Suspense;kp-025 的 useActionState 把「提交中/失败」也纳入类似范式。

延伸阅读

  • Suspense API:<https://react.dev/reference/react/Suspense>
  • Error Boundaries:<https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary>
  • react-error-boundary:<https://github.com/bvaughn/react-error-boundary>