React 错误边界实战:用 ErrorBoundary 兜住渲染崩溃并做降级 UI
2026/7/23 3:19:06 网站建设 项目流程

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 className="card">{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 不是万能捕获

这是最多人误解的地方。错误边界只捕获渲染期间子组件的错误,以下四类它一律抓不到:

  1. 事件处理函数里的错误(onClick、onChange 等)
  2. 异步代码(setTimeout、Promise.then、fetch 回调)
  3. 服务端渲染(SSR)期间的错误
  4. 错误边界组件自己抛出的错误

原因是这些场景不在 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,它把上面这些都封好了,还支持onResetresetKeys(依赖变化自动重置)、FallbackComponent:

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} onReset={() => refetch()} // 重置时重新拉数据 resetKeys={[userId]} // userId 变了自动重置边界 > <UserCard userId={userId} /> </ErrorBoundary>

resetKeys尤其实用:切换用户后自动清掉上一个用户的错误态,不用手动点重试。

小结

  • 错误边界的价值是把渲染崩溃控制在局部子树,而不是让一个组件带崩整页白屏。
  • 实现只能用 class 组件:getDerivedStateFromError出降级 UI,componentDidCatch做上报。
  • 粒度要分层:全局兜底 + 局部(卡片/区块)独立边界,让失败范围符合用户预期。
  • 抓不到的四类:事件处理、异步、SSR、边界自身——这些要自己 try/catch,必要时用「渲染期 setState throw」转交边界。
  • 给降级 UI 加 reset,配合resetKeys实现自愈;生产直接上react-error-boundary省事。

一句话记忆:错误边界只兜「渲染期、子组件、同步」的错;事件和异步的错,得你自己接住再决定要不要抛给它。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询