React错误边界

354 阅读5分钟

componentDidCatch 的作用

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

componentDidCatch 的签名

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

componentDidCatch 的限制

  1. 只能捕获渲染过程中的错误: componentDidCatch 只能捕获在渲染阶段,或者生命周期方法中,子组件抛出的错误。它无法捕获以下类型的错误:
    • 事件处理函数中的错误:例如 onClickonSubmit 等。
    • 异步代码中的错误: 例如 setTimeout, Promise.catchasync/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: 初始化 hasErrorfalse,以及 errorerrorInfo为 null.
    • getDerivedStateFromError(error): 这是一个静态方法,它在子组件抛出错误后被调用。它更新 state,设置 hasErrortrue
    • componentDidCatch(error, info):
      • 捕获错误对象和组件栈信息。
      • 输出错误信息到控制台。
      • 更新 state,保存 errorInfo 到 state 中。
    • render():
      • 如果 hasErrortrue,则渲染一个友好的降级 UI,显示错误信息和组件堆栈。
      • 否则,渲染 this.props.children,也就是它的子组件。
  2. BrokenComponent 组件:
    • 如果 shouldThrow prop 为 true,则抛出一个错误,模拟组件渲染过程中出错的情况。否则正常渲染。
  3. AsyncComponent 组件
    • componentDidMount 中 使用setTimeout 模拟异步请求,并且捕获了异常
    • 状态更新 data
  4. EventComponent 组件
    • handleClick 事件中捕获错误
  5. NormalComponent 组件
    • 一个正常渲染的组件,不会抛出错误
  6. MultipleComponents 组件
    • 模拟渲染多个错误子组件,观察错误边界捕获
  7. App 组件:
    • 在主应用组件中,NormalComponentBrokenComponentErrorBoundary 包裹。
    • 同时渲染了 AsyncComponentEventComponent,以及 MultipleComponents 用于验证错误边界的边界。

运行效果

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

总结

  • componentDidCatch 是 React 中用于处理子组件渲染错误的强大工具。
  • 它只能捕获渲染过程中的同步错误。
  • 它不会捕获事件处理函数、异步代码或错误边界组件自身的错误。
  • 正确使用错误边界可以提高应用的健壮性和用户体验。

函数式组件错误边界

由于 componentDidCatch 是类组件的生命周期方法,函数组件本身并没有对应的直接 API。但是,我们可以借助 React Hooks 和一些技巧来实现类似的功能。

核心概念:使用 useStateuseEffect 创建自定义错误边界 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 函数,重置错误边界状态。这样可以多次捕获错误。
    • 如果 hasErrortrue,则渲染 fallback 组件或者一个默认的降级 UI,显示错误信息和组件堆栈。
    • 如果 hasErrorfalse, 正常渲染子组件,并且通过 React.cloneElement 为每一个子组件添加 handleError props, 用于子组件捕获错误,并且通知父组件错误发生。
  3. BrokenComponent 组件(函数组件):
    • 如果 shouldThrowtrue, 不直接抛出错误,而是调用 handleError 函数,传递错误信息,并将错误信息传递给 父组件的错误边界 ErrorBoundary
  4. AsyncComponent 组件
    • useEffect 中使用 setTimeout模拟异步操作,并且捕获了异步错误,并且更新状态 data
  5. EventComponent 组件
    • handleClick 事件中捕获错误
  6. NormalComponent 组件
    • 一个正常渲染的组件,不会抛出错误
  7. MultipleComponents 组件
    • 模拟渲染多个错误子组件,观察错误边界捕获
  8. App 组件:
    • 使用 ErrorBoundary 组件包裹其他子组件。

运行效果

  1. BrokenComponentshouldThrowtrue 时,ErrorBoundary 会捕获这个错误,并显示降级 UI。你可以在控制台中看到错误信息和组件堆栈。
  2. BrokenComponentshouldThrowfalse 时,正常渲染。
  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 可以更好地在函数组件中进行状态管理和错误处理。