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

569 阅读4分钟

一. 简单的容错组件

封装

import React from 'react';
 
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
 
  static getDerivedStateFromError(error) {
    // 更新状态,以便下次渲染可以显示后备的UI
    return { hasError: true };
  }
 
  componentDidCatch(error, errorInfo) {
    // 你同样可以将错误信息发送到一个错误报告服务
    logErrorToMyService(error, errorInfo);
  }
 
  render() {
    if (this.state.hasError) {
      // 你可以渲染任何自定义的后备UI
      return <h1>Something went wrong.</h1>;
    }
 
    return this.props.children; 
  }
}

使用

<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>

二. 使用 react-error-boundary

下载

npm install react-error-boundary

使用

import React from 'react';
import ErrorBoundary from 'react-error-boundary';
 
const MyApp = () => (
  <ErrorBoundary
    FallbackComponent={({ error, resetErrorBoundary }) => (
      <div>
        There was an error: <pre>{error.message}</pre>
        <button onClick={resetErrorBoundary}>Try again</button>
      </div>
    )}
    onReset={() => {
      // 处理重置逻辑
    }}
  >
    <Suspense fallback={<div>Loading...</div>}>
      <MyBrokenComponent />
    </Suspense>
  </ErrorBoundary>
);

三. 分析 react-error-boundary

捕获错误的 Api

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
    );
  }
	
  // ...
}

重置组件

将错误信息重置使得能渲染原组件

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
    );
  }
	
  // ...
}

通过 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;
}

在 ErrorBoundary 中捕获异步错误

  • 错误边界只捕获在 React 渲染周期中发生的错误。在它之外发生的事情,如 reject 的 Promise、带有 setTimeout的异步代码、各种回调和事件处理程序抛出的错误无法捕捉
  • Dan Abramov 与我们分享了一个很酷的黑科技,正是为了实现这一点。先用 try/catch 捕捉这些错误,然后在 catch 语句中触发正常的 React 重渲染,然后把这些错误重新抛回重渲染周期
const Component = () => {
  // create some random state that we'll use to throw errors
  const [state, setState] = useState();

  const onClick = () => {
    try {
      // something bad happened
    } catch (e) {
      // trigger state update, with updater function as an argument
      setState(() => {
        // re-throw this error within the updater function
        // it will be triggered during state update
        throw e;
      })
    }
  }
}