学完这篇,你能写出一个真正好用的 React 错误边界,知道该把它放在哪一层,也能避开「加了边界还是白屏」的常见坑。
第一步:写出最小可用的错误边界
错误边界目前只有类组件能定义,核心是两个生命周期:
class ErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error }; // 渲染阶段兜底,必须同步返回新 state
}
componentDidCatch(error, info) {
console.error(error, info.componentStack); // 这里做上报,别改 state
}
render() {
if (this.state.hasError) {
return this.props.fallback ?? <p>这段内容加载出错了</p>;
}
return this.props.children;
}
}
用法就是把它包在需要保护的子树外面:
<ErrorBoundary fallback={<p>侧边栏挂了</p>}>
<Sidebar />
</ErrorBoundary>
getDerivedStateFromError 负责切到兜底 UI,componentDidCatch 负责记录。两者分工不要混:前者必须是纯函数,后者才允许副作用。
注意:getDerivedStateFromError 里不要写 console.log、请求、setState,它会在渲染阶段被调用,产生副作用会导致行为不可预期。
第二步:给兜底 UI 加上「重试」
错误边界一旦进入错误态,如果不重置 state 就会永远停在兜底页。最省事的做法是加一个重置方法,并允许父组件通过 key 强制重建:
reset = () => this.setState({ hasError: false, error: null });
render() {
if (this.state.hasError) {
return (
<div>
<p>加载失败</p>
<button onClick={this.reset}>重试</button>
</div>
);
}
return this.props.children;
}
父组件侧可以这样用:
<ErrorBoundary key={userId} fallback={<Error />}>
<Profile userId={userId} />
</ErrorBoundary>
切换用户时 key 变化,组件重建,错误态自动清空。
注意:如果错误是因为数据本身有问题(比如接口一直返回脏数据),用户点「重试」还是会立刻再次报错。重试按钮最好同时触发一次数据重新拉取,而不是只清 state。
第三步:函数组件项目直接用 react-error-boundary
手写类组件在 Hooks 项目里很别扭。社区方案 react-error-boundary 已经把常用能力封装好了:
npm i react-error-boundary
import { ErrorBoundary } from 'react-error-boundary';
function Fallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>出错了:{error.message}</p>
<button onClick={resetErrorBoundary}>重试</button>
</div>
);
}
<ErrorBoundary
FallbackComponent={Fallback}
onError={(error, info) => reportToServer(error, info.componentStack)}
onReset={() => queryClient.invalidateQueries()}
resetKeys={[userId]}
>
<Profile userId={userId} />
</ErrorBoundary>
resetKeys 里的值变化时会自动重置,比手动传 key 更直观;onReset 用来清理缓存、重新请求。
第四步:决定边界放在哪一层
这是最容易做错的地方。只在根节点包一层,等于任何一个角落报错都会整页白屏。
合理的分层是:
- 路由层:每个路由页面一个边界,一个页面挂了不影响导航和全局布局
- 区块层:侧栏、评论区、图表、第三方嵌入内容,各自独立包裹
- 不包裹:按钮点击、表单提交这类交互,用 try/catch 就够了
<Layout>
<ErrorBoundary fallback={<PageError />}>
<ErrorBoundary fallback={<SidebarError />}>
<Sidebar />
</ErrorBoundary>
<ErrorBoundary fallback={<ContentError />}>
<Routes />
</ErrorBoundary>
</ErrorBoundary>
</Layout>
第五步:知道它捕获不到什么
错误边界只处理「渲染期间」抛出的错误,以下场景它管不了:
| 场景 | 处理方式 |
|---|
事件处理器 onClick 里报错 | 自己 try/catch |
setTimeout / Promise 回调 | window.onunhandledrejection |
| 服务端渲染 | 服务端自己的错误处理 |
| 错误边界自身抛错 | 上一层的边界接住 |
useEffect(() => {
const handler = (e) => reportToServer(e.reason);
window.addEventListener('unhandledrejection', handler);
return () => window.removeEventListener('unhandledrejection', handler);
}, []);
注意:生产环境的兜底 UI 不要直接展示 error.stack 或后端原始错误信息,容易泄露路径和内部结构。展示一个错误 ID,把详情送到监控平台就好。
小结
- 错误边界必须是类组件,靠
getDerivedStateFromError 切 UI、componentDidCatch 做上报
- 一定要提供重置手段:
resetKeys、key 或重置按钮
- 边界要分层放,路由级 + 区块级,别只包根节点
- 函数组件项目用
react-error-boundary,省掉样板代码
- 它只捕获渲染期错误,异步和事件回调要另接
unhandledrejection 与 try/catch
- 兜底 UI 不要暴露错误堆栈,给用户一个 ID,给监控完整堆栈