React 错误边界组件 react-error-boundary 源码解析

捕获错误 hook

  • getDerivedStateFromError
    • 返回值会作为组件的 state 用于展示错误时的内容
  • componentDidCatch

创建错误边界组件 Provider

  • 错误边界组件其实是一个通过 Context.Provider 包裹的组件,这样使得组件内部可以获取到捕捉的相关操作
import {
    createContext } from "react";

export type ErrorBoundaryContextType = {
   
  didCatch: boolean;
  error: any;
  resetErrorBoundary: (...args: any[]) => void;
};

// 错误边界组件其实是一个通过 Context.Provider 包裹的组件
export const ErrorBoundaryContext =
  createContext<ErrorBoundaryContextType | null>(null);

定义错误边界组件

定义边界组件状态

type ErrorBoundaryState =
  | {
   
      didCatch: true;
      error: any;
    }
  | {
   
      didCatch: false;
      error: null;
    };

const initialState: ErrorBoundaryState = {
   
  didCatch: false, // 错误是否捕捉
  error: null, // 捕捉到的错误信息
};

捕捉错误

  • getDerivedStateFromError 捕捉到错误后,设置组件状态展示备份组件
  • componentDidCatch 用于触发错误回调
export class ErrorBoundary extends Component<
  ErrorBoundaryProps,
  ErrorBoundaryState
> {
   
  constructor(props: ErrorBoundaryProps) {
   
    super(props);

    this.resetErrorBoundary = this.resetErrorBoundary.bind(this);
    this.state = initialState;
  }
  
  
  static getDerivedStateFromError(error: Error) {
   
    return {
    didCatch: true, error };
  }
  
  componentDidCatch(error: Error, info: ErrorInfo) {
   
    this.props.onError?.(error, info);
  }

}

渲染备份组件

  • 通过指定的参数名区分是无状态组件还是有状态组件
    • 无状态组件通过直接调用函数传递 props
    • 有状态组件通过 createElement 传递 props
  • 通过 createElement 处理传递的组件更加优雅
    • createElement(元素类型,参数,子元素)详情,其中第一个参数可以直接传递 Context.Provider
export class ErrorBoundary extends Component<
  ErrorBoundaryProps,
  ErrorBoundaryState
> {
   

  // ...
  
  render() {
   
    const {
    children, fallbackRender, FallbackComponent, fallback } =
      this.props;
    const {
    didCatch, error } = this.state;

    let childToRender = children;
	// 如果捕捉到了错误
    if (didCatch) {
   
      const props: FallbackProps = {
   
        error,
        resetErrorBoundary: this.resetErrorBoundary,
      };
	  // 通过指定的参数名区分是无状态组件还是有状态组件
      if (typeof fallbackRender === "function") {
   
        childToRender = fallbackRender(props);
      } else if (FallbackComponent) {
   
        childToRender = createElement(FallbackComponent, props);
      } else if (fallback === null || isValidElement(fallback)) {
   
        childToRender = fallback;
      } else {
   
        if (isDevelopment) {
   
          console.error(
            "react-error-boundary requires either a fallback, fallbackRender, or FallbackComponent prop"
          );
        }

        throw error;
      }
    }
	
	// Context.Provider 可以直接作为 createElement 的第一个参数
    return createElement(
      ErrorBoundaryContext.Provider,
      {
   
        value: {
    // Context.Provider 提供可供消费的内容
          didCatch,
          error,
          resetErrorBoundary: this.resetErrorBoundary,
        },
      },
      childToRender
    );
  }
	
  // ...
}

重置组件

  • 将错误信息重置使得能渲染原组件
const initialState: ErrorBoundaryState = {
   
  didCatch: false, // 错误是否捕捉
  error: null, // 捕捉到的错误信息
};

export class ErrorBoundary extends Component<
  ErrorBoundaryProps,
  ErrorBoundaryState
> {
   

  // ...
  resetErrorBoundary(...args: any[]) {
   
    const {
    error } = this.state;

    if (error !== null) {
   
      this.props.onReset?.({
    // 触发对应回调
        args,
        reason: "imperative-api",
      });

      this.setState(initialState);
    }
  }
  // ...
  
  // 根据 resetKeys 重置,但并未对外暴露该 API
  componentDidUpdate(
    prevProps: ErrorBoundaryProps,
    prevState: ErrorBoundaryState
  ) {
   
    const {
    didCatch } = this.state;
    const {
    resetKeys } = this.props;

    // There's an edge case where if the thing that triggered the error happens to *also* be in the resetKeys array,
    // we'd end up resetting the error boundary immediately.
    // This would likely trigger a second error to be thrown.
    // So we make sure that we don't check the resetKeys on the first call of cDU after the error is set.

    if (
      didCatch &&
      prevState.error !== null &&
      hasArrayChanged(prevProps.resetKeys, resetKeys)
    ) {
   
      this.props.onReset?.({
   
        next: resetKeys,
        prev: prevProps.resetKeys,
        reason: "keys",
      });

      this.setState(initialState);
    }
  }
}

function hasArrayChanged(a: any[] = [], b: any[] = []) {
   
  return (
    a.length !== b.length || a.some((item, index) => !Object.is(item, b[index]))
  );
}

通过 useHook 控制边界组件

  • 通过 context 获取最近的边界组件内容
  • 通过手动抛出错误重新触发边界组件
import {
    useContext, useMemo, useState } from "react";
import {
    assertErrorBoundaryContext } from "./assertErrorBoundaryContext";
import {
    ErrorBoundaryContext } from "./ErrorBoundaryContext";

type UseErrorBoundaryState<TError> =
  | {
    error: TError; hasError: true }
  | {
    error: null; hasError: false };

export type UseErrorBoundaryApi<TError> = {
   
  resetBoundary: () => void;
  showBoundary: (error: TError) => void;
};

export function useErrorBoundary<TError = any>(): UseErrorBoundaryApi<TError> {
   

  // 获取最近的边界组件 Provider 的内容
  const context = useContext(ErrorBoundaryContext);
  
  // 断言 Context 是否为空
  assertErrorBoundaryContext(context);

  const [state, setState] = useState<UseErrorBoundaryState<TError>>({
   
    error: null,
    hasError: false,
  });

  const memoized = useMemo(
    () => ({
   
      resetBoundary: () => {
   
        // 提供 Provider 对应的重置边界组件方法,渲染原组件
        context.resetErrorBoundary();
        setState({
    error: null, hasError: false });
      },
      // 手动抛出错误,触发边界组件
      showBoundary: (error: TError) =>
        setState({
   
          error,
          hasError: true,
        }),
    }),
    [context.resetErrorBoundary]
  );
  // 当调用 showBoundary 后,该 hook 会手动抛出错误,让边界组件来捕捉
  if (state.hasError) {
   
    throw state.error;
  }

  return memoized;
}

相关推荐

  1. React 错误边界组件 react-error-boundary

    2024-02-06 07:20:06       38 阅读
  2. 使用 Error Boundary 捕获 React 组件错误

    2024-02-06 07:20:06       19 阅读
  3. React@16.x(18)错误边界

    2024-02-06 07:20:06       9 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-02-06 07:20:06       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-02-06 07:20:06       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-02-06 07:20:06       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-02-06 07:20:06       20 阅读

热门阅读

  1. golang开源定时任务调度框架

    2024-02-06 07:20:06       31 阅读
  2. 这是为什么啊!

    2024-02-06 07:20:06       30 阅读
  3. Rust消费kafka

    2024-02-06 07:20:06       32 阅读
  4. Spring boot 集成redis

    2024-02-06 07:20:06       29 阅读
  5. 【案例】--分布式”雪花算法案例

    2024-02-06 07:20:06       31 阅读
  6. Springboot使用kafka的两种方式

    2024-02-06 07:20:06       33 阅读
  7. 百度语音合成API

    2024-02-06 07:20:06       29 阅读
  8. Vue 本地存储

    2024-02-06 07:20:06       30 阅读