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组件:- 如果
shouldThrowprop 为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;
详细解释
useErrorBoundaryHook:hasError:状态,表示是否发生错误。error: 状态,存储错误对象。errorInfo: 状态,存储错误组件信息(componentStack)。resetError: 重置错误状态handleError: 错误处理函数,当子组件抛出错误时,更新错误状态。- 这个 Hook 返回以上状态和错误处理函数。
ErrorBoundary组件(函数组件):- 使用
useErrorBoundaryHook 来获取错误状态和处理函数。 useEffect: 监听hasError状态,当hasError变为 true 时,调用resetError函数,重置错误边界状态。这样可以多次捕获错误。- 如果
hasError为true,则渲染fallback组件或者一个默认的降级 UI,显示错误信息和组件堆栈。 - 如果
hasError为false, 正常渲染子组件,并且通过React.cloneElement为每一个子组件添加handleErrorprops, 用于子组件捕获错误,并且通知父组件错误发生。
- 使用
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 捕获。 - 事件和异步错误: 和类组件的错误边界一样,函数组件中的错误边界也无法捕获事件处理函数、异步操作的错误。
fallbackprop: 添加fallbackprop,允许用户自定义降级后的 UI。
总结
- 通过自定义 Hook 和函数组件,我们也可以实现错误边界功能,让函数组件拥有错误处理能力。
- 错误边界必须在渲染阶段或者生命周期中进行捕获,对于事件或者异步错误,需要使用
try...catch自行处理。 - 使用自定义 Hook 可以更好地在函数组件中进行状态管理和错误处理。