It’s now or never

IT系の技術ブログです。気になったこと、勉強したことを備忘録的にまとめて行きます。

ErrorBoundaryについて理解する

Suspenseについての簡単なおさらい

Suspenseを使って非同期処理を含むReactコンポーネントを内包すると、Suspenseは未完了のPromiseをcatchして、完了まではfallbackのコンポーネントを表示してくれる。

inon29.hateblo.jp 参照。

ErrorBoundary

Suspenseがchildrenとして内包するコンポーネントは、 Promiseを処理する課程でエラー(例外)を発生する可能性がある。

その時一般的には、Errorオブジェクトがthrowされるが、レンダリング最中にPromiseを処理するようになったReact18以降では、このエラーをキャッチする仕組みとしてErrorBoundaryというコンポーネントを使用する。

基本的な使い方

<ErrorBoundary fallback={"Something went wrong"}>
  <Suspense fallback={"loading..."}>
    <SuspendedComponent />
  </Suspense>
</ErrorBoundary>

基本的な使い方はSuspenseコンポーネント同様で、childrenで例外がthrowされた場合は、fallbackのコンポーネントが表示される。

ErrorBoundaryコンポーネント

ErrorBoundaryコンポーネントは、Suspenseとは異なり、Reactが提供しているコンポーネントではなく、自前で良いしなくてはならない。

どのように実装すれば良いかは、公式ドキュメント に記載されている。

公式が提示しているサンプルコードは以下のとおり。

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // 子コンポーネントで例外が発生すると呼ばれる。
    // 戻り値では、コンポーネントの状態を返す必要がある。
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    // 発生したエラーのログを取得する
    logErrorToMyService(error, info.componentStack);
  }

  render() {
    if (this.state.hasError) {
      // エラーがある場合は、fallbackをレンダリングする
      return this.props.fallback;
    }
    // エラーがない場合は、childrenをレンダリングする
    return this.props.children;
  }
}

重要なイベント関数は [static getDerivedStateFromError](https://react.dev/reference/react/Component#static-getderivedstatefromerror)

この関数を持つコンポーネントの子コンポーネントが例外をthrowしたときにこの関数が呼ばれる。

関数の引数として、throwされた例外を受け取り。処理後の状態を戻り値として返す。

[componentDidCatch(error, info)](https://react.dev/reference/react/Component#componentdidcatch) という関数もあり、こちらもgetDerivedStateFromErrorと同様に例外発生時に呼び出されるが、現在はこの関数は主にログなど情報取得などに使うことを目的としており、状態の更新には推奨されない。

Function コンポーネントでErrorBoundaryを実装することはまだできない。

現時点で、FunctionコンポーネントとしてErrorBoundaryを実装することはできず、Classコンポーネントを使う必要がる。

クラスコンポーネントをアプリケーションで独自管理するのが嫌な場合は、ErrorBoundaryの実装を提供してくれている react-error-boundary のようなossを使用する。

ErrorBoundaryがキャッチしないエラー

Suspenseを使って、統一された設計をしていれば、起きない問題なのかもしれないが、ErrorBoundaryではキャッチできない例外が幾つかある。

古いドキュメントではあるが以下のようなものが該当する。

error boundary は以下のエラーをキャッチしません:

イベントハンドラ(詳細)
非同期コード(例:setTimeout や requestAnimationFrame のコールバック)
サーバサイドレンダリング
(子コンポーネントではなく)error boundary 自身がスローしたエラー

よくあるパターンとして、useEffectからPromiseなどで通信処理を行う実装があるが、このようなときに発生したエラーも処理できない。

react-error-boundaryの実装参考

react-error-boundaryでは、SuspenseをつかないケースやuseEffect内やその他非同期ケースでのエラーを補足するために、hooksも提供してくれている。(useErrorBoundary)

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

export function useErrorBoundary<Error = any>(): UseErrorBoundaryApi<Error> {
  const context = useContext(ErrorBoundaryContext);

  assertErrorBoundaryContext(context);

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

  const memoized = useMemo(
    () => ({
      resetBoundary: () => {
        context?.resetErrorBoundary();
        setState({ error: null, hasError: false });
      },
      showBoundary: (error: Error) =>
        setState({
          error,
          hasError: true,
        }),
    }),
    [context?.resetErrorBoundary]
  );

  if (state.hasError) {
    throw state.error;
  }

  return memoized;
}

hooksが提供しているエラー発生の関数はshowBoundary

showBoundary: (error: Error) =>
        setState({
          error,
          hasError: true,
        })
 if (state.hasError) {
    throw state.error;
 }

作りは非常にシンプルで、showBoundaryを非同期処理中の例外発生時で呼び出すとhooksのレンダリング中に例外としてthrowしてくれるというもの。

吐き出された例外はレンダリング中にthrowされるため、ErrorBoundaryで補足されるようになる。

感想

Suspenseコンポーネントを使う場合は、ErrorBoundaryはセットの実装となるが、ErrorBoundary自体は単体のデザインとして実装に組み込める。

既存プロダクトの導入として、Suspenseへの移行は段階的に置き換え計画が必要だが、ErrorBoundaryは比較的置き換えが用意のため、まずはこちらからという方針でも良さそう。

(実際に参画中のプロダクトではErrorBoundaryだけをまずは導入している。)