React 错误边界实战:用 ErrorBoundary 兜住渲染崩溃并做降级 UI
React 错误边界实战:用 ErrorBoundary 兜住渲染崩溃并做降级 UI线上突然白屏,控制台一句Cannot read properties of undefined,整个页面全没了——只是某个卡片组件拿到一条脏数据,却把整棵 React 树带崩了。这就是没有错误边界的代价:一个子组件抛错,React 会卸载整个组件树,用户看到的是纯白页面而不是「这块内容加载失败」。这篇讲清楚 ErrorBoundary 能兜什么、不能兜什么,以及怎么写一个真正可用、能上报、能重试的错误边界。为什么一个组件出错会白屏React 16 之后有个明确的行为:渲染阶段(render、生命周期、构造函数)里抛出的未捕获错误,会导致整个根组件树被卸载。设计初衷是——一个损坏的 UI 比一个错误的 UI 更安全,不能让用户在数据错乱的界面上继续操作(比如转错账)。但「安全」不等于「白屏」。我们需要的是把爆炸范围控制在出错的那个子树,其余部分照常工作。这就是错误边界的职责。先看一个必崩的例子:function UserCard({ user }) { // user.profile 为 undefined 时,这行直接抛 TypeError return div classNamecard{user.profile.nickname}/div; } function Page() { return ( div h1用户列表/h1 UserCard user{{ id: 1 }} / {/* 缺 profile 字段 */} /div ); }UserCard抛错,连带h1用户列表/h1一起消失,整页白。错误边界怎么写到今天(React 19)为止,错误边界只能用 class 组件实现,没有等价的官方 Hook。关键是两个方法:static getDerivedStateFromError(error):渲染阶段触发,返回新 state 用来渲染降级 UI。componentDidCatch(error, info):提交阶段触发,适合做副作用(上报日志),拿得到组件栈。import { Component } from react; class ErrorBoundary extends Component { state { hasError: false }; // 捕获到错误后,更新 state 触发降级 UI 渲染 static getDerivedStateFromError() { return { hasError: true }; } // 只做副作用:上报,不要在这里 setState 改 UI(那是上面那个方法的活) componentDidCatch(error, info) { reportError(error, info.componentStack); } render() { if (this.state.hasError) { return this.props.fallback ?? p这块内容加载失败了/p; } return this.props.children; } }用的时候,把它包在可能出错、又不想连累全局的子树外面:function Page() { return ( div h1用户列表/h1 ErrorBoundary fallback{p用户卡片挂了,其他内容不受影响/p} UserCard user{{ id: 1 }} / /ErrorBoundary /div ); }现在UserCard再崩,h1和页面其余部分完好,只有卡片位置显示降级文案。边界放哪里:粒度决定体验新手常犯的错:整个 App 只在最外层包一个 ErrorBoundary。这样确实不白屏了,但任何一个小组件出错,整页都变成「加载失败」,体验一样差。正确做法是分层放:ErrorBoundary fallback{FullPageError /} {/* 兜底,防真·全局崩溃 */} Header / ErrorBoundary fallback{WidgetError /} {/* 侧边栏单独兜 */} Sidebar / /ErrorBoundary main {items.map((item) ( ErrorBoundary key{item.id} fallback{CardError /} Card data{item} / {/* 每张卡片独立兜 */} /ErrorBoundary ))} /main /ErrorBoundary一张卡片的脏数据只会毁掉它自己,列表其余卡片照常渲染。把边界放在「用户能接受局部失败」的自然分块处,是错误边界设计的核心。关键的坑:ErrorBoundary 不是万能捕获这是最多人误解的地方。错误边界只捕获渲染期间子组件的错误,以下四类它一律抓不到:事件处理函数里的错误(onClick、onChange 等)异步代码(setTimeout、Promise.then、fetch 回调)服务端渲染(SSR)期间的错误错误边界组件自己抛出的错误原因是这些场景不在 React 的渲染调用栈里,React 的 try/catch 包不住它们。看个例子:function BuyButton() { const handleClick () { // 这个错误 ErrorBoundary 抓不到!会变成未处理异常 throw new Error(下单失败); }; return button onClick{handleClick}购买/button; }事件里的错误得自己 try/catch,用 state 记录再渲染降级:function BuyButton() { const [err, setErr] useState(null); const handleClick async () { try { await placeOrder(); } catch (e) { setErr(e); // 转成渲染态,如果需要还能抛给边界 } }; if (err) return p下单失败,请重试/p; return button onClick{handleClick}购买/button; }如果确实想让异步错误也走统一的错误边界,有个技巧:在渲染期把错误重新抛出去。function useThrowAsync() { const [, setState] useState(); // 返回一个函数,调用它会在下一次渲染时把 error 抛到边界 return useCallback((error) { setState(() { throw error; }); }, []); } function BuyButton() { const throwToBoundary useThrowAsync(); const handleClick async () { try { await placeOrder(); } catch (e) { throwToBoundary(e); // 交给外层 ErrorBoundary 统一处理 } }; return button onClick{handleClick}购买/button; }setState的 updater 里 throw,会在渲染阶段抛出,这时就落进 ErrorBoundary 的捕获范围了。加上「重试」:让降级 UI 能自愈只显示「加载失败」还不够,用户想点一下重来。给边界加一个 reset 能力:class ErrorBoundary extends Component { state { hasError: false }; static getDerivedStateFromError() { return { hasError: true }; } reset () this.setState({ hasError: false }); render() { if (this.state.hasError) { // 把 reset 传给 fallback,让用户能主动重试 return this.props.fallback(this.reset); } return this.props.children; } } // 用法 ErrorBoundary fallback{(retry) ( div p卡片加载失败/p button onClick{retry}重试/button /div )} Card data{item} / /ErrorBoundary点「重试」把hasError复位,children 重新渲染。配合外部数据重新拉取(比如 React Query 的resetErrorBoundary),就能真正恢复。不想自己写:react-error-boundary生产项目里更推荐直接用社区库react-error-boundary,它把上面这些都封好了,还支持onReset、resetKeys(依赖变化自动重置)、FallbackComponent:import { ErrorBoundary } from react-error-boundary; function Fallback({ error, resetErrorBoundary }) { return ( div rolealert p出错了:{error.message}/p button onClick{resetErrorBoundary}重试/button /div ); } ErrorBoundary FallbackComponent{Fallback} onReset{() refetch()} // 重置时重新拉数据 resetKeys{[userId]} // userId 变了自动重置边界 UserCard userId{userId} / /ErrorBoundaryresetKeys尤其实用:切换用户后自动清掉上一个用户的错误态,不用手动点重试。小结错误边界的价值是把渲染崩溃控制在局部子树,而不是让一个组件带崩整页白屏。实现只能用 class 组件:getDerivedStateFromError出降级 UI,componentDidCatch做上报。粒度要分层:全局兜底 局部(卡片/区块)独立边界,让失败范围符合用户预期。抓不到的四类:事件处理、异步、SSR、边界自身——这些要自己 try/catch,必要时用「渲染期 setState throw」转交边界。给降级 UI 加 reset,配合resetKeys实现自愈;生产直接上react-error-boundary省事。一句话记忆:错误边界只兜「渲染期、子组件、同步」的错;事件和异步的错,得你自己接住再决定要不要抛给它。