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

文章目录

捕获错误 hook

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

创建错误边界组件 Provider

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

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

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

定义错误边界组件

定义边界组件状态

javascript 复制代码
type ErrorBoundaryState =
  | {
      didCatch: true;
      error: any;
    }
  | {
      didCatch: false;
      error: null;
    };

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

捕捉错误

  • getDerivedStateFromError 捕捉到错误后,设置组件状态展示备份组件
  • componentDidCatch 用于触发错误回调
javascript 复制代码
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
javascript 复制代码
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
    );
  }
	
  // ...
}

重置组件

  • 将错误信息重置使得能渲染原组件
javascript 复制代码
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 获取最近的边界组件内容
  • 通过手动抛出错误重新触发边界组件
javascript 复制代码
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;
}
相关推荐
01传说5 分钟前
vue3 配置安装 pnpm 报错 已解决
java·前端·vue.js·前端框架·npm·node.js
Misha韩11 分钟前
React Native 一些API详解
react native·react.js
小李飞飞砖11 分钟前
React Native 组件间通信方式详解
javascript·react native·react.js
小李飞飞砖12 分钟前
React Native 状态管理方案全面对比
javascript·react native·react.js
烛阴1 小时前
Python装饰器解除:如何让被装饰的函数重获自由?
前端·python
千鼎数字孪生-可视化1 小时前
Web技术栈重塑HMI开发:HTML5+WebGL的轻量化实践路径
前端·html5·webgl
凌辰揽月1 小时前
7月10号总结 (1)
前端·css·css3
天天扭码2 小时前
很全面的前端面试——CSS篇(上)
前端·css·面试
EndingCoder2 小时前
搜索算法在前端的实践
前端·算法·性能优化·状态模式·搜索算法
sunbyte2 小时前
50天50个小项目 (Vue3 + Tailwindcss V4) ✨ | DoubleVerticalSlider(双垂直滑块)
前端·javascript·css·vue.js·vue