
一句话:Error Boundary 只能由类组件实现——依赖
static getDerivedStateFromError(渲染降级 UI)与componentDidCatch(上报),并要提供「重置」能力;用「包装组件 + 函数式 fallback」可以让使用方是函数组件。
一、为什么必须用类组件
Error Boundary 的能力是「在【渲染失败时】替换渲染结果」——
这要求一个「有状态、可被 React 在渲染失败时调用」的实体。
而 Hooks(useState/useEffect)都在「渲染成功之后」才运行,
【无法介入「渲染抛错」这个时机】。
所以 React 规定:
· `getDerivedStateFromError(error)` —— 静态方法,在「渲染阶段」被调用,返回新 state
· `componentDidCatch(error, info)` —— 在「提交阶段」被调用,可做副作用(上报)
✅ 但可以让「使用方」是函数组件(把类组件封装成「包装组件」)
二、基础实现
import { Component, type ErrorInfo, type ReactNode } from 'react';
interface Props {
children: ReactNode;
/** 降级 UI(可以是「静态节点」或「函数」——函数能拿到 error 与 reset) */
fallback: ReactNode | ((error: Error, reset: () => void) => ReactNode);
/** 错误回调(用于上报) */
onError?: (error: Error, info: ErrorInfo) => void;
}
interface State {
error: Error | null;
}
export class ErrorBoundary extends Component<Props, State> {
state: State = { error: null };
/** ① 渲染阶段:返回「新的 state」→ 触发降级 UI 的渲染 */
static getDerivedStateFromError(error: Error): State {
return { error };
}
/** ② 提交阶段:做副作用(上报、日志) */
componentDidCatch(error: Error, info: ErrorInfo) {
// ⭐ info.componentStack 是【定位 React 错误的核心】信息
console.error('[ErrorBoundary]', error, info.componentStack);
this.props.onError?.(error, info);
}
/** ③ 重置(否则降级 UI 会一直显示) */
private reset = () => {
this.setState({ error: null });
};
render() {
const { error } = this.state;
const { children, fallback } = this.props;
if (error) {
return typeof fallback === 'function' ? <>{fallback(error, this.reset)}</> : <>{fallback}</>;
}
return <>{children}</>;
}
}
// ✅ 使用
function App() {
return (
<ErrorBoundary
onError={(error, info) => reportError(error, { componentStack: info.componentStack })}
fallback={(error, reset) => (
<div role="alert" className="error-page">
<h2>出错了</h2>
{error.message}
<button onClick={reset}>重试</button>
<button onClick={() => (window.location.href = '/')}>返回首页</button>
</div>
)}
>
<Page />
</ErrorBoundary>
);
}
⭐ 基础版的三个关键点:
① `getDerivedStateFromError` 必须是【静态】【纯函数】(不能调 API)
② 只有 `componentDidCatch` 里能做副作用(上报)
③ 必须提供 `reset`(否则用户只能刷新页面)
三、完整实现(含 resetKeys 与「重挂载」)
import { Component, type ErrorInfo, type ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback: ReactNode | ((error: Error, reset: () => void) => ReactNode);
onError?: (error: Error, info: ErrorInfo) => void;
/** 重置时的回调(清理状态,如「清空查询缓存」) */
onReset?: () => void;
/** ⭐ 这些值变化时自动重置(如「路由变化」「用户切换」时自动恢复) */
resetKeys?: unknown[];
/** 重置时是否「强制重新挂载子树」(推荐 true —— 保证内部状态被清掉) */
remountOnReset?: boolean;
}
interface State {
error: Error | null;
/** 用于「强制重新挂载」的 key */
resetCount: number;
}
export class ErrorBoundary extends Component<Props, State> {
state: State = { error: null, resetCount: 0 };
static getDerivedStateFromError(error: Error): Partial<State> {
return { error };
}
componentDidCatch(error: Error, info: ErrorInfo) {
this.props.onError?.(error, info);
// ⚠️ 不要在这里 setState(已经通过 getDerivedStateFromError 设置了)
}
componentDidUpdate(prevProps: Props) {
// ⭐ resetKeys 变化时自动重置
const { resetKeys } = this.props;
if (this.state.error === null || !resetKeys) return;
const keysChanged =
!prevProps.resetKeys ||
resetKeys.length !== prevProps.resetKeys.length ||
resetKeys.some((k, i) => !Object.is(k, prevProps.resetKeys![i]));
if (keysChanged) this.resetErrorBoundary();
}
resetErrorBoundary = () => {
this.props.onReset?.();
this.setState((prev) => ({ error: null, resetCount: prev.resetCount + 1 }));
};
render() {
const { error, resetCount } = this.state;
const { children, fallback, remountOnReset = true } = this.props;
if (error) {
return typeof fallback === 'function'
? <>{fallback(error, this.resetErrorBoundary)}</>
: <>{fallback}</>;
}
// ⭐ 用 key 强制「重新挂载子树」——保证出错的内部状态被清掉
return <div key={remountOnReset ? resetCount : undefined}>{children}</div>;
}
}
// ✅ 使用:路由变化时「自动重置」
function App() {
const location = useLocation();
return (
<ErrorBoundary
resetKeys={[location.pathname]} // ⭐ 切路由时自动重置
onReset={() => queryClient.resetQueries()} // ⭐ 重置时清缓存
fallback={(error, reset) => <ErrorPage error={error} onRetry={reset} />}
>
<Routes />
</ErrorBoundary>
);
}
四、用「包装组件」让使用方是函数组件
/** ✅ 一个「函数式 API」的包装(内部仍是类组件) */
interface ErrorBoundaryProps {
children: ReactNode;
fallback: (args: { error: Error; reset: () => void }) => ReactNode;
onError?: (error: Error, info: ErrorInfo) => void;
onReset?: () => void;
resetKeys?: unknown[];
}
export function ErrorBoundaryWrapper({ children, fallback, ...rest }: ErrorBoundaryProps) {
return (
<ErrorBoundary
{...rest}
fallback={(error, reset) => fallback({ error, reset })}
>
{children}
</ErrorBoundary>
);
}
// ✅ 使用(函数式)
<ErrorBoundaryWrapper
resetKeys={[userId]}
fallback={({ error, reset }) => (
<div role="alert">
<p>加载 {userId} 失败:{error.message}</p>
<button onClick={reset}>重试</button>
</div>
)}
>
<UserProfile userId={userId} />
</ErrorBoundaryWrapper>
五、分层放置 + 局部隔离
// ✅ 推荐结构:应用级兜底 + 区域级隔离
function App() {
return (
<ErrorBoundaryWrapper
fallback={({ error, reset }) => <FullPageError error={error} onRetry={reset} />} // ① 最后兜底
onError={reportError}
>
<TopNav />
<main>
<ErrorBoundaryWrapper fallback={({ reset }) => <WidgetError onRetry={reset} />}> {/* ② 区域 A */}
<Dashboard />
</ErrorBoundaryWrapper>
<ErrorBoundaryWrapper fallback={({ reset }) => <WidgetError onRetry={reset} />}> {/* ③ 区域 B */}
<RecommendList />
</ErrorBoundaryWrapper>
</main>
<Footer />
</ErrorBoundaryWrapper>
);
}
// ✅ 专门隔离「可能崩溃的第三方内容」(如用户输入的富文本、第三方图表)
function SafeRender({ children, raw }: { children: ReactNode; raw: string }) {
return (
<ErrorBoundaryWrapper
fallback={() => (
<div className="fallback">
<p>内容渲染失败,已显示原始文本</p>
<pre>{raw}</pre> {/* ⭐ 降级方案:显示原始文本而不是崩溃 */}
</div>
)}
>
{children}
</ErrorBoundaryWrapper>
);
}
<SafeRender raw={content}>
<RichTextRenderer content={content} />
</SafeRender>;
六、错误恢复策略
⭐ 「重试」时要考虑「状态是否已被污染」:
① 只替换渲染(默认)
· 出错的子树被卸载,内部状态丢失
· 但「全局状态/缓存」可能仍处于不一致状态
→ 适合「一次性渲染错误」(如某个数据字段为空导致的崩溃)
② 清理相关缓存后重试
· 在 onReset 里调用「清缓存」(`queryClient.resetQueries()`)
· 适合「数据导致的错误」(缓存里存了坏数据)
③ 强制重新挂载(remountOnReset = true)
· 用 key 变化让子树「完全重建」
· 保证「不会带着坏状态重试」
→ **推荐默认开启**
④ 降级到「只读/简化模式」
· 如「富文本渲染失败 → 显示原始 markdown」
· 适合「非关键功能」(用户仍能完成主流程)
// ✅ 一个完整的「重置 + 重挂载 + 清缓存」实现
function useErrorReset() {
const queryClient = useQueryClient();
return useCallback(() => {
queryClient.resetQueries(); // 清掉可能的坏缓存
// 其它需要重置的全局状态
}, [queryClient]);
}
七、与 Suspense / 数据路由的配合
// ✅ 标准顺序:ErrorBoundary 在外、Suspense 在内(见 8.4)
<ErrorBoundaryWrapper fallback={({ error, reset }) => <ErrorView error={error} onRetry={reset} />}>
<Suspense fallback={<Skeleton />}>
<LazyOrAsyncContent />
</Suspense>
</ErrorBoundaryWrapper>
// ✅ 数据路由:errorElement 处理「loader/action 抛出的错误」
// (它也能处理渲染错误,且可以「按路由层级」配置)
const router = createBrowserRouter([
{
path: 'users/:id',
element: <UserDetail />,
loader: async ({ params }) => {
const user = await fetchUser(params.id!);
if (!user) throw new Response('Not Found', { status: 404 }); // ⭐ 交给 errorElement
return user;
},
errorElement: <UserError />, // 用 useRouteError + isRouteErrorResponse 读错误
},
]);
| 错误类型 | 用什么 |
|---|---|
| 组件渲染错误 | Error Boundary |
loader/action 抛出的错误 |
数据路由的 errorElement |
| 事件处理里的错误 | try/catch(或 useErrorBoundary 手动上抛) |
| 异步/Promise 错误 | catch + unhandledrejection 全局监听 |
| 接口错误 | 请求层拦截器 |
八、常见坑
// ① 以为它能捕获「事件处理里的错误」
<button onClick={() => { throw new Error(); }} /> // ❌ 捕获不到
// ② 以为它能捕获「异步/Promise 的错误」
Promise.reject(new Error()); // ❌
// ③ 以为它能捕获「useEffect 里的错误」(React 18 起捕获不到)
useEffect(() => { throw new Error('x'); }, []); // ❌
// ④ 只实现 getDerivedStateFromError 而不提供「重置」
// → 降级 UI 永远显示,用户只能刷新
// ⑤ fallback 只是一行「出错了」(没有出路)
fallback={<div>出错了</div>} // ⚠️
// ⑥ 只放一个最外层(局部错误 → 整页崩)
// ⑦ fallback 里再抛错 → 需要「外层再包一层」
// ⑧ 忘记「上报」(Error Boundary 的一半价值是可观测性)
// ⑨ 在生产环境「把 error.stack 显示给用户」(泄露内部实现)
// ⑩ 重试时「不清理状态」(带着坏状态重试 → 又崩)
// ⑪ 用 try/catch 包 JSX(语法上行不通,捕获不到)
try { return <Comp />; } catch {} // ❌
// ⑫ 认为「React 18 用 try/catch 替代了 Error Boundary」(没有)
// ⑬ 把 Error Boundary 放在「会被卸载的位置」(如 Modal 内部)→ 起不到兜底作用
// ⑭ 在 StrictMode 下「错误被捕获两次」(上报要去重,见 8.6)
// ⑮ 忘记「componentDidCatch 里不要再 setState」(已经设置过 state 了)
// ⑯ 用 Error Boundary 代替「输入校验」(应该提前校验而不是靠崩溃兜底)
// ⑰ 「重置」后没有重新请求数据(用户看到的是旧数据或空)
// ✅ 在 onReset 里触发 refetch / invalidate
// ⑱ 认为「Error Boundary 能恢复应用状态」(它只替换渲染)
// ⑰ 的完整处理
<ErrorBoundaryWrapper
onReset={() => {
queryClient.invalidateQueries({ queryKey: ['user'] }); // ⭐ 重置时重新拉数据
}}
fallback={({ error, reset }) => <ErrorView error={error} onRetry={reset} />}
>
<UserProfile />
</ErrorBoundaryWrapper>
// ⑨ 的生产环境写法:只显示「友好信息」,详细堆栈只上报
function ErrorView({ error, onRetry }: { error: Error; onRetry: () => void }) {
const isDev = import.meta.env.DEV;
return (
<div role="alert">
<h2>页面出错了</h2>
<p>请重试,或稍后再访问</p>
{isDev && <pre>{error.stack}</pre>} {/* ⭐ 只在开发环境显示堆栈 */}
<button onClick={onRetry}>重试</button>
<button onClick={() => window.location.reload()}>刷新页面</button>
</div>
);
}
九、面试延伸
- 「手写一个 Error Boundary」
用类组件实现(因为「在渲染失败时替换渲染结果」必须有状态且能被 React 在渲染阶段调用):①static getDerivedStateFromError(error)——渲染阶段被调用,必须是静态纯函数,返回新的 state(用于渲染降级 UI);②componentDidCatch(error, info)——提交阶段被调用,可以做副作用(上报错误,info.componentStack是定位 React 错误的核心信息);③reset()——提供重置能力(否则降级 UI 会一直显示);④render里按state.error决定渲染fallback还是children。 -
「为什么 Error Boundary 只能用类组件?」
因为它的核心能力是「在渲染抛错时替换渲染结果」——这要求一个「有状态、可被 React 在「渲染失败」这个时机调用」的实体。而 Hooks(useState/useEffect)都在「渲染成功之后」才运行,无法介入「渲染失败」这个时机(useState的初始化函数抛错时,组件函数本身就没返回)。所以 React 规定「Error Boundary 必须是类组件」;但可以用「包装组件」(把类组件包成一个 props 是函数的组件)让使用方是函数组件。 -
「Error Boundary 能捕获哪些、不能捕获哪些错误?」
能捕获:子组件在「渲染」「生命周期方法」「构造函数」中抛出的错误(含所有后代)。不能捕获:① 事件处理函数里的错误(需要try/catch);② 异步代码(setTimeout、Promise、async函数);③useEffect里抛出的错误(React 18 起不捕获);④ 服务端渲染中的错误;⑤ Error Boundary 自身抛出的错误(需要外层再包一层)。所以「渲染错误用 Error Boundary,其它错误用全局监听/try-catch」(见 8.6)。 -
「为什么
getDerivedStateFromError必须是静态纯函数?」
因为它在「渲染阶段」被调用——而渲染阶段要满足「纯函数、可被中断、可被重放」(见 4.3)。所以它:① 必须是static(不能访问this,避免依赖实例状态导致不确定性);② 不能有副作用(不能调 API、不能改外部状态,否则会被重复执行);③ 只能「根据 error 计算出新的 state」。真正的副作用(上报、日志)应该放在componentDidCatch(提交阶段,只调用一次)。 -
「Error Boundary 应该放在哪里?」
建议分层放置:① 应用级(最外层,最后兜底,显示「整个页面出错」+ 刷新/返回);② 区域级(每个独立功能块,如「仪表盘」「推荐列表」,一个区域崩了不影响其它区域);③ 第三方内容级(富文本、图表、地图——容易因异常数据崩溃);④ 路由级(每个页面独立兜底,数据路由下用errorElement)。只放一个最外层的问题是「任何局部错误都会让整页变成错误页」,用户体验很差。 -
「『重试』要注意什么?」
三条:① 清理可能被污染的状态——错误可能来自「缓存里的坏数据」,所以onReset里应该invalidateQueries/resetQueries;② 强制重新挂载——用「改变key」让子树完全重建(避免「带着坏状态重试 → 又崩」),推荐作为默认行为(remountOnReset);③resetKeys——在「某些值变化时自动重置」(如「路由变化」「用户切换」),避免用户被卡在错误页。此外「重试后要重新请求数据」(否则显示的还是旧数据)。 -
「Error Boundary 能恢复应用状态吗?」
不能——它只做「替换渲染」。错误发生后:① 出错的子树被卸载,其内部状态丢失;② 但「其它地方的状态」(全局 store、TanStack Query 缓存)可能仍处于不一致的状态(比如「缓存里存了导致崩溃的坏数据」)。所以「恢复」需要主动做:① 清缓存(queryClient.resetQueries());② 重置相关全局状态;③ 重新请求数据;④ 必要时「强制重新挂载子树」。 -
「Error Boundary 和
errorElement(数据路由)有什么区别?」
职责不同:① Error Boundary 是「React 本身的机制」——捕获「渲染、生命周期、构造函数」中的错误(只能是类组件实现);②errorElement是「数据路由的机制」——它除了「渲染错误」外还能捕获「loader/action抛出的错误」(尤其是throw new Response(..., { status: 404 })这类「路由级错误」),并且可以「按路由层级配置」(最近的errorElement生效),还能用useRouteError+isRouteErrorResponse读取结构化的错误信息。实践上两者一起用:路由级错误交给errorElement,组件内部的局部错误用 Error Boundary 隔离。
一句话速记
Error Boundary 只能用类组件实现(因为要「在渲染失败时替换结果」):
static getDerivedStateFromError(渲染阶段、纯函数、返回新 state) +componentDidCatch(提交阶段、可上报,拿info.componentStack) + 必须提供reset;只能捕获「渲染 / 生命周期 / 构造函数」的错误,「事件处理、异步、useEffect、SSR、自身」都捕获不到;要分层放置(应用级 + 区域级 + 第三方内容级),只放一层会让局部错误变整页崩;重试三条是「清缓存(resetQueries)、强制重挂载(用key)、resetKeys自动重置」;生产环境别显示error.stack(用import.meta.env.DEV判断)但要上报;与 Suspense 的顺序是「ErrorBoundary 在外、Suspense 在内」;路由级错误用errorElement(能捕获 loader/action 的错误),与 Error Boundary 配合使用。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!