一句话定义
<Suspense fallback> 声明「子树没准备好时显示什么」(加载中),Error Boundary 声明「子树抛错时显示什么」(降级 UI)——两者都是声明式的状态容器,把 loading/error 从业务代码中剥离出来。
为什么重要
手写 loading && <Spinner/> 分支会把每个数据组件污染成三态机器;而 Error Boundary 无法用函数组件自己实现(必须类组件)。掌握这两件工具是从「能跑的页面」到「健壮的产品」的门槛,也是理解 RSC 流式渲染(kp-024)的前置课。
前置知识
核心概念
- 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>包装即可。
自测题
- 「渲染中抛出的错误」「事件处理器错误」「effect 中的错误」三者谁会被 Boundary 捕获?其余怎么处理?
- 写出「页面级 lazy + Suspense + ErrorBoundary」的组合结构,并说明三者职责。
- 嵌套 Suspense 如何实现「先出骨架的模块先显示」?
- 为什么说 Error Boundary 不能用函数组件自身实现(提示:它需要在渲染失败后仍能渲染,函数组件没有这个生命周期位)。
- 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>