React错误边界

555 阅读5分钟

componentDidCatch 的作用

componentDidCatch 是 React 组件的生命周期方法之一,它的主要作用是捕获在其子组件渲染过程中发生的 JavaScript 错误。它就像一个 “错误边界”,防止整个应用因为某个子组件的错误而崩溃。

componentDidCatch 的签名

componentDidCatch(error, info) {
  // 处理错误
}
  • error: 捕获到的错误对象。
  • info: 包含有关错误发生组件信息的对象,通常包含 componentStack 属性,显示组件的调用栈。

componentDidCatch 的限制

  1. 只能捕获渲染过程中的错误: componentDidCatch 只能捕获在渲染阶段,或者生命周期方法中,子组件抛出的错误。它无法捕获以下类型的错误:
    • 事件处理函数中的错误:例如 onClick、onSubmit 等。
    • 异步代码中的错误: 例如 setTimeout, Promise.catch 或 async/await 中的错误。
    • 错误边界组件自身的错误
    • 服务端渲染中的错误
    • React 事件系统本身抛出的错误。
  2. 必须是类组件: componentDidCatch 只能在类组件中使用。函数组件没有对应的 API。
  3. 单个错误边界不会捕获子组件的错误: 如果你有一个错误边界组件,它下面有多个子组件,如果多个子组件都抛出错误,则错误边界只会捕捉到第一个错误。
  4. 错误不会冒泡到更高的错误边界: 错误边界只会捕捉其子组件的错误,不会捕捉到父组件的错误。错误边界是本地的。

代码示例

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;

详细解释

  1. 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,也就是它的子组件。
  2. BrokenComponent 组件:
    • 如果 shouldThrow prop 为 true,则抛出一个错误,模拟组件渲染过程中出错的情况。否则正常渲染。
  3. AsyncComponent 组件
    • componentDidMount 中 使用setTimeout 模拟异步请求,并且捕获了异常
    • 状态更新 data
  4. EventComponent 组件
    • 在 handleClick 事件中捕获错误
  5. NormalComponent 组件
    • 一个正常渲染的组件,不会抛出错误
  6. MultipleComponents 组件
    • 模拟渲染多个错误子组件,观察错误边界捕获
  7. App 组件:
    • 在主应用组件中,NormalComponent 和 BrokenComponent 用 ErrorBoundary 包裹。
    • 同时渲染了 AsyncComponent 和 EventComponent,以及 MultipleComponents 用于验证错误边界的边界。

运行效果

  1. 当 BrokenComponent 的 shouldThrow 为 true 时,ErrorBoundary 会捕获这个错误,并显示降级 UI。你可以在控制台中看到错误信息和组件堆栈。
  2. 当 BrokenComponent 的 shouldThrow 为 false 时,正常渲染。
  3. AsyncComponent 组件的异步操作中的错误被其自身内部的 try...catch 捕获,并且更新了状态,错误边界不会捕获该错误。
  4. EventComponent 的事件处理函数中的错误,也被 try...catch 捕获,错误边界不会捕获该错误。
  5. 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;

详细解释

  1. useErrorBoundary Hook:
    • hasError:状态,表示是否发生错误。
    • error: 状态,存储错误对象。
    • errorInfo: 状态,存储错误组件信息(componentStack)。
    • resetError: 重置错误状态
    • handleError: 错误处理函数,当子组件抛出错误时,更新错误状态。
    • 这个 Hook 返回以上状态和错误处理函数。
  2. ErrorBoundary 组件(函数组件):
    • 使用 useErrorBoundary Hook 来获取错误状态和处理函数。
    • useEffect: 监听 hasError 状态,当 hasError 变为 true 时,调用 resetError 函数,重置错误边界状态。这样可以多次捕获错误。
    • 如果 hasError 为 true,则渲染 fallback 组件或者一个默认的降级 UI,显示错误信息和组件堆栈。
    • 如果 hasError 为 false, 正常渲染子组件,并且通过 React.cloneElement 为每一个子组件添加 handleError props, 用于子组件捕获错误,并且通知父组件错误发生。
  3. BrokenComponent 组件(函数组件):
    • 如果 shouldThrow 为 true, 不直接抛出错误,而是调用 handleError 函数,传递错误信息,并将错误信息传递给 父组件的错误边界 ErrorBoundary。
  4. AsyncComponent 组件
    • useEffect 中使用 setTimeout模拟异步操作,并且捕获了异步错误,并且更新状态 data
  5. EventComponent 组件
    • 在 handleClick 事件中捕获错误
  6. NormalComponent 组件
    • 一个正常渲染的组件,不会抛出错误
  7. MultipleComponents 组件
    • 模拟渲染多个错误子组件,观察错误边界捕获
  8. App 组件:
    • 使用 ErrorBoundary 组件包裹其他子组件。

运行效果

  1. 当 BrokenComponent 的 shouldThrow 为 true 时,ErrorBoundary 会捕获这个错误,并显示降级 UI。你可以在控制台中看到错误信息和组件堆栈。
  2. 当 BrokenComponent 的 shouldThrow 为 false 时,正常渲染。
  3. AsyncComponent 组件的异步操作中的错误被其自身内部的 try...catch 捕获,并且更新了状态,错误边界不会捕获该错误。
  4. EventComponent 的事件处理函数中的错误,也被 try...catch 捕获,错误边界不会捕获该错误。
  5. MultipleComponents 组件虽然渲染了多个抛出错误的 BrokenComponent 组件,但是错误边界只会捕获到第一个 BrokenComponent 的错误。

重要说明

  • 错误捕获机制:函数组件无法使用 componentDidCatch 直接捕获错误,因此我们使用 handleError 捕获错误,并且通知父组件的错误边界进行处理。
  • 错误捕获位置:handleError 错误捕获函数必须在子组件的渲染阶段或者生命周期中调用,因为函数组件只能在渲染期间捕获子组件的错误。异步操作中的错误或事件函数中的错误需要使用 try...catch 捕获。
  • 事件和异步错误: 和类组件的错误边界一样,函数组件中的错误边界也无法捕获事件处理函数、异步操作的错误。
  • fallback prop: 添加 fallback prop,允许用户自定义降级后的 UI。

总结

  • 通过自定义 Hook 和函数组件,我们也可以实现错误边界功能,让函数组件拥有错误处理能力。
  • 错误边界必须在渲染阶段或者生命周期中进行捕获,对于事件或者异步错误,需要使用 try...catch 自行处理。
  • 使用自定义 Hook 可以更好地在函数组件中进行状态管理和错误处理。