componentDidCatch
的作用
componentDidCatch
是 React 组件的生命周期方法之一,它的主要作用是捕获在其子组件渲染过程中发生的 JavaScript 错误。它就像一个 “错误边界”,防止整个应用因为某个子组件的错误而崩溃。
componentDidCatch
的签名
componentDidCatch(error, info) {
// 处理错误
}
error
: 捕获到的错误对象。info
: 包含有关错误发生组件信息的对象,通常包含componentStack
属性,显示组件的调用栈。
componentDidCatch
的限制
- 只能捕获渲染过程中的错误:
componentDidCatch
只能捕获在渲染阶段,或者生命周期方法中,子组件抛出的错误。它无法捕获以下类型的错误:- 事件处理函数中的错误:例如
onClick
、onSubmit
等。 - 异步代码中的错误: 例如
setTimeout
,Promise.catch
或async/await
中的错误。 - 错误边界组件自身的错误
- 服务端渲染中的错误
- React 事件系统本身抛出的错误。
- 事件处理函数中的错误:例如
- 必须是类组件:
componentDidCatch
只能在类组件中使用。函数组件没有对应的 API。 - 单个错误边界不会捕获子组件的错误: 如果你有一个错误边界组件,它下面有多个子组件,如果多个子组件都抛出错误,则错误边界只会捕捉到第一个错误。
- 错误不会冒泡到更高的错误边界: 错误边界只会捕捉其子组件的错误,不会捕捉到父组件的错误。错误边界是本地的。
代码示例
import React, { Component } from 'react';
// 错误边界组件
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染可以显示降级后的 UI
return { hasError: true, error: error };
}
componentDidCatch(error, info) {
// 你也可以将错误日志上报给错误跟踪服务
console.error('Error caught by ErrorBoundary:', error, info);
this.setState({
errorInfo: info
})
}
render() {
if (this.state.hasError) {
// 你可以自定义降级后的 UI
return (
<div>
<h2>Something went wrong.</h2>
<p>Error: {this.state.error && this.state.error.toString()}</p>
{this.state.errorInfo && (
<details style={{ whiteSpace: 'pre-wrap' }}>
{this.state.errorInfo.componentStack}
</details>
)}
</div>
);
}
return this.props.children;
}
}
// 可能抛出错误的组件
class BrokenComponent extends Component {
render() {
if (this.props.shouldThrow) {
//故意抛出一个错误
throw new Error('Oops! I broke.');
}
return <div>正常组件</div>
}
}
// 异步操作中可能抛出错误的组件
class AsyncComponent extends Component {
state = {
data: null
}
componentDidMount() {
// 模拟异步操作
setTimeout(() => {
try {
//模拟异步获取数据失败
throw new Error("异步加载数据失败");
// this.setState({
// data: "成功获取数据"
// })
}
catch (e) {
console.error("错误在 AsyncComponent中被捕获", e);
this.setState({
data: e.message
})
}
}, 1000);
}
render () {
return <div>{this.state.data ? `数据: ${this.state.data}` : "加载中..."}</div>;
}
}
// 事件处理中可能抛出错误的组件
class EventComponent extends Component {
handleClick = () => {
try {
throw new Error('Error in click handler!');
}
catch (e) {
console.error('捕获错误:', e)
}
};
render() {
return <button onClick={this.handleClick}>Click me</button>
}
}
// 一个普通组件
class NormalComponent extends Component {
render () {
return <div>正常渲染</div>;
}
}
// 模拟多组件渲染,观察错误边界捕获
function MultipleComponents () {
return (
<ErrorBoundary>
<BrokenComponent shouldThrow={true}/>
<BrokenComponent shouldThrow={true}/>
</ErrorBoundary>
)
}
// 主应用组件
class App extends Component {
render() {
return (
<div>
<h1>React Error Boundaries</h1>
<ErrorBoundary>
<NormalComponent />
<BrokenComponent shouldThrow={false} />
<BrokenComponent shouldThrow={true}/>
</ErrorBoundary>
<AsyncComponent/>
<EventComponent/>
<MultipleComponents/>
</div>
);
}
}
export default App;
详细解释
ErrorBoundary
组件:constructor
: 初始化hasError
为false
,以及error
和errorInfo
为 null.getDerivedStateFromError(error)
: 这是一个静态方法,它在子组件抛出错误后被调用。它更新state
,设置hasError
为true
。componentDidCatch(error, info)
:- 捕获错误对象和组件栈信息。
- 输出错误信息到控制台。
- 更新
state
,保存errorInfo
到 state 中。
render()
:- 如果
hasError
为true
,则渲染一个友好的降级 UI,显示错误信息和组件堆栈。 - 否则,渲染
this.props.children
,也就是它的子组件。
- 如果
BrokenComponent
组件:- 如果
shouldThrow
prop 为true
,则抛出一个错误,模拟组件渲染过程中出错的情况。否则正常渲染。
- 如果
AsyncComponent
组件componentDidMount
中 使用setTimeout
模拟异步请求,并且捕获了异常- 状态更新
data
EventComponent
组件- 在
handleClick
事件中捕获错误
- 在
NormalComponent
组件- 一个正常渲染的组件,不会抛出错误
MultipleComponents
组件- 模拟渲染多个错误子组件,观察错误边界捕获
App
组件:- 在主应用组件中,
NormalComponent
和BrokenComponent
用ErrorBoundary
包裹。 - 同时渲染了
AsyncComponent
和EventComponent
,以及MultipleComponents
用于验证错误边界的边界。
- 在主应用组件中,
运行效果
- 当
BrokenComponent
的shouldThrow
为true
时,ErrorBoundary
会捕获这个错误,并显示降级 UI。你可以在控制台中看到错误信息和组件堆栈。 - 当
BrokenComponent
的shouldThrow
为false
时,正常渲染。 AsyncComponent
组件的异步操作中的错误被其自身内部的try...catch
捕获,并且更新了状态,错误边界不会捕获该错误。EventComponent
的事件处理函数中的错误,也被try...catch
捕获,错误边界不会捕获该错误。MultipleComponents
组件虽然渲染了多个抛出错误的BrokenComponent
组件,但是错误边界只会捕获到第一个BrokenComponent
的错误。
总结
componentDidCatch
是 React 中用于处理子组件渲染错误的强大工具。- 它只能捕获渲染过程中的同步错误。
- 它不会捕获事件处理函数、异步代码或错误边界组件自身的错误。
- 正确使用错误边界可以提高应用的健壮性和用户体验。
函数式组件错误边界
由于 componentDidCatch
是类组件的生命周期方法,函数组件本身并没有对应的直接 API。但是,我们可以借助 React Hooks 和一些技巧来实现类似的功能。
核心概念:使用 useState
和 useEffect
创建自定义错误边界 Hook
我们将创建一个自定义 Hook,它利用 useState
来存储错误状态,并利用 useEffect
监听子组件是否抛出错误,并捕获它。
代码示例
import React, { useState, useEffect, useCallback } from 'react';
// 自定义错误边界 Hook
function useErrorBoundary() {
const [hasError, setHasError] = useState(false);
const [error, setError] = useState(null);
const [errorInfo, setErrorInfo] = useState(null);
const resetError = useCallback(() => {
setHasError(false);
setError(null);
setErrorInfo(null);
}, []);
const handleError = useCallback((error, info) => {
// 你也可以将错误日志上报给错误跟踪服务
console.error('Error caught by ErrorBoundary Hook:', error, info);
setHasError(true);
setError(error);
setErrorInfo(info);
}, []);
// 返回错误状态,错误信息,错误堆栈, 以及错误处理函数
return { hasError, error, errorInfo, handleError, resetError };
}
// 错误边界组件(函数组件)
function ErrorBoundary({ children, fallback }) {
const { hasError, error, errorInfo, handleError, resetError } = useErrorBoundary();
useEffect(() => {
if(hasError) {
resetError()
}
}, [hasError, resetError])
if (hasError) {
// 你可以自定义降级后的 UI
return fallback ? fallback({error, errorInfo, resetError}) : (
<div>
<h2>Something went wrong.</h2>
<p>Error: {error && error.toString()}</p>
{errorInfo && (
<details style={{ whiteSpace: 'pre-wrap' }}>
{errorInfo.componentStack}
</details>
)}
</div>
);
}
// 正常渲染子组件,捕获子组件的错误
return (
React.Children.map(children, child => {
if (React.isValidElement(child)) {
return React.cloneElement(child, {
handleError: handleError
})
}
return child;
})
);
}
// 可能抛出错误的组件(函数组件)
function BrokenComponent({shouldThrow, handleError}) {
if (shouldThrow) {
// throw new Error('Oops! I broke.'); // 直接抛出错误无法捕获
// 捕获渲染过程中的错误
handleError(new Error("Oops! I broke."), {componentStack: "BrokenComponent"});
return null;
}
return <div>正常组件</div>
}
// 异步操作中可能抛出错误的组件
function AsyncComponent() {
const [data, setData] = useState(null)
useEffect(() => {
setTimeout(() => {
try {
//模拟异步获取数据失败
throw new Error("异步加载数据失败");
// setData("成功获取数据")
}
catch (e) {
console.error("错误在 AsyncComponent中被捕获", e);
setData(e.message)
}
}, 1000);
}, []);
return <div>{data ? `数据: ${data}` : "加载中..."}</div>;
}
// 事件处理中可能抛出错误的组件
function EventComponent() {
const handleClick = () => {
try {
throw new Error('Error in click handler!');
}
catch (e) {
console.error('捕获错误:', e)
}
};
return <button onClick={handleClick}>Click me</button>
}
// 一个普通组件(函数组件)
function NormalComponent() {
return <div>正常渲染</div>;
}
// 模拟多组件渲染,观察错误边界捕获
function MultipleComponents () {
return (
<ErrorBoundary>
<BrokenComponent shouldThrow={true}/>
<BrokenComponent shouldThrow={true}/>
</ErrorBoundary>
)
}
// 主应用组件
function App() {
return (
<div>
<h1>React Error Boundaries (Function Component)</h1>
<ErrorBoundary>
<NormalComponent />
<BrokenComponent shouldThrow={false} />
<BrokenComponent shouldThrow={true}/>
</ErrorBoundary>
<AsyncComponent/>
<EventComponent/>
<MultipleComponents/>
</div>
);
}
export default App;
详细解释
useErrorBoundary
Hook:hasError
:状态,表示是否发生错误。error
: 状态,存储错误对象。errorInfo
: 状态,存储错误组件信息(componentStack
)。resetError
: 重置错误状态handleError
: 错误处理函数,当子组件抛出错误时,更新错误状态。- 这个 Hook 返回以上状态和错误处理函数。
ErrorBoundary
组件(函数组件):- 使用
useErrorBoundary
Hook 来获取错误状态和处理函数。 useEffect
: 监听hasError
状态,当hasError
变为 true 时,调用resetError
函数,重置错误边界状态。这样可以多次捕获错误。- 如果
hasError
为true
,则渲染fallback
组件或者一个默认的降级 UI,显示错误信息和组件堆栈。 - 如果
hasError
为false
, 正常渲染子组件,并且通过React.cloneElement
为每一个子组件添加handleError
props, 用于子组件捕获错误,并且通知父组件错误发生。
- 使用
BrokenComponent
组件(函数组件):- 如果
shouldThrow
为true
, 不直接抛出错误,而是调用handleError
函数,传递错误信息,并将错误信息传递给 父组件的错误边界ErrorBoundary
。
- 如果
AsyncComponent
组件useEffect
中使用 setTimeout模拟异步操作,并且捕获了异步错误,并且更新状态data
EventComponent
组件- 在
handleClick
事件中捕获错误
- 在
NormalComponent
组件- 一个正常渲染的组件,不会抛出错误
MultipleComponents
组件- 模拟渲染多个错误子组件,观察错误边界捕获
App
组件:- 使用
ErrorBoundary
组件包裹其他子组件。
- 使用
运行效果
- 当
BrokenComponent
的shouldThrow
为true
时,ErrorBoundary
会捕获这个错误,并显示降级 UI。你可以在控制台中看到错误信息和组件堆栈。 - 当
BrokenComponent
的shouldThrow
为false
时,正常渲染。 AsyncComponent
组件的异步操作中的错误被其自身内部的try...catch
捕获,并且更新了状态,错误边界不会捕获该错误。EventComponent
的事件处理函数中的错误,也被try...catch
捕获,错误边界不会捕获该错误。MultipleComponents
组件虽然渲染了多个抛出错误的BrokenComponent
组件,但是错误边界只会捕获到第一个BrokenComponent
的错误。
重要说明
- 错误捕获机制:函数组件无法使用
componentDidCatch
直接捕获错误,因此我们使用handleError
捕获错误,并且通知父组件的错误边界进行处理。 - 错误捕获位置:
handleError
错误捕获函数必须在子组件的渲染阶段或者生命周期中调用,因为函数组件只能在渲染期间捕获子组件的错误。异步操作中的错误或事件函数中的错误需要使用 try...catch 捕获。 - 事件和异步错误: 和类组件的错误边界一样,函数组件中的错误边界也无法捕获事件处理函数、异步操作的错误。
fallback
prop: 添加fallback
prop,允许用户自定义降级后的 UI。
总结
- 通过自定义 Hook 和函数组件,我们也可以实现错误边界功能,让函数组件拥有错误处理能力。
- 错误边界必须在渲染阶段或者生命周期中进行捕获,对于事件或者异步错误,需要使用
try...catch
自行处理。 - 使用自定义 Hook 可以更好地在函数组件中进行状态管理和错误处理。