※このブログではサーバー運用、技術の検証等の費用のため広告をいれています。
記事が見づらいなどの問題がありましたらContactからお知らせください。


【React in TypeScript】ErrorBoundariesの使用方法と注意点

react web開発 フロント開発 TypeScript ErrorBoundaries Error webフレームワーク

投稿日:2021年2月11日

このエントリーをはてなブックマークに追加
ErrorBoundariesとは子孫要素で発生したエラーの処理をするために作成されたコンポーネントです。エラー発生したことをユーザーに知らせたり、エラーログを作成したりするために使用します。この記事ではErrorBoundariesコンポーネントの使い方・ユースケース・使用の際の注意点について解説しています。

はじめに

ErrorBoundariesとは

ErrorBoundariesとは子孫要素で発生したエラーの処理をするために作成されたコンポーネントです。エラー発生したことをユーザーに知らせたり、エラーログを作成したりするために使用します。

ErrorBoundariesはあくまでReactのクラスコンポーネントの機能をつかって自分で実装するコンポーネントであることを覚えておきましょう。

環境

この記事のコードは以下の環境で確認されました。

  • TypeScript version 4.1.2
  • React version 17.0.1

実践

以下のような条件を満たすコンポーネントとErrorBoundariesと呼びます。

  • getDerivedStateFromError()静的メソッドとcomponentDidCatch(error: Error, errorInfo: ErrorInfo) メソッドのどちらか片方、あるいはその両方を実装している。
  • render()メソッドでエラーが発生していない場合のみ子要素(this.props.children)をレンダリングする。

実際にErrorBoundariesコンポーネントから作成しましょう。コンポーネント名はわかりやすくErrorBoundaryにしておきます。

./components/ErrorBoundary.tsx
import React, { ErrorInfo } from "react";

/**
 * 子要素でエラーが発生した際に取得し、その情報をコンソールログに出力する
 * エラーが発生したことはユーザーに知らせる
 */
class ErrorBoundary extends React.Component<{}, { hasError: boolean }> {
  constructor(props: {}) {
    super(props);
    this.state = {
      hasError: false,
    };
  }
  static getDerivedStateFromError(): { hasError: boolean } {
    console.log("getDerivedStatefromErrorがよばれました。");
    return { hasError: true };
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
    console.log(error);
    console.log(errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return <div>エラーが発生しました</div>;
    }
    return <>{this.props.children}</>;
  }
}

export default ErrorBoundary;

getDerivedStateFromError()メソッドはその名の通りエラーをもとにstateに値をセットするためのメソッドです。返り値はstateの更新値のオブジェクトです。おそらく、ライフサイクル側で返し値のオブジェクトでsetState()が呼ばれているのだと思います。ユーザーに対してエラーが発生していることを知らせるためのstate更新をするのがいいでしょう。

componentDidCatch()はエラー発生時にエラー情報を引数として呼ばれます。第1引数には発生したError、第2引数には発生したエラー情報のオブジェクトが渡されます。サイト管理者が利用するログの作成、エラーの発生を知らせるために使用しましょう。

render()メソッド内で、this.state.hasErrorの値を条件として返すReactノードを分岐させていることに注目しましょう。this.state.hasErrorのbool値は初期値がfalsegetDerivedStateFromError()メソッドが呼ばれた際に、trueにセットされるように記述しました。そのため、一度でも子孫要素のレンダリングでエラーが発生するとエラーの発生を知らせる要素の描画に分岐されます。

次に、意図的にエラーを発生させるサンプルのコンポーネントを作成します。

./components/SampleComponent.tsx
import React from "react";

/**
 * ユーザーデータの構造定義
 */
interface UserData {
  user: {
    name: string;
    age: number;
  };
}

/**
 * APIの呼び出しのMock
 */
const callAPIMock = async (): Promise<UserData> => {
  const sampleResponse = '{"username": "marsquai", "age": 120}';
  return JSON.parse(sampleResponse);
};

/**
 * サンプルコンポーネント
 * ユーザーデータを取得して情報を表示する
 *
 */
const SampleComponent = () => {
  const [userData, setUserData] = React.useState<UserData | undefined>(
    undefined
  );
  callAPIMock().then((data) => {
    setUserData(data);
  });
  if (userData) {
    // 想定しているデータと違う構造であるため必ずエラーが発生する
    return (
      <div>
        <div>{userData.user.name}</div>
        <div>{userData.user.age}</div>
      </div>
    );
  } else {
    return <div>データ取得中</div>;
  }
};

export default SampleComponent;

ここではまずcallAPIMock()というAPIの呼び出しを模倣する非同期関数を作成しました。この関数の中でJSONの文字列をパースしているのですが、肝心のJSON文字列が返し値の型と一致していないというミスがあります。これが今回のエラーの発生原因になります。

SampleComponentというコンポーネントではcallAPIMock()関数を呼び出し、結果が返された際にuserDataという変数にデータを渡します。

userData変数はUserDataインターフェイスの型を想定しているのですが、callAPIMock()の処理はミスによりUserDataと違う型のオブジェクトが返されます。render()メソッドではuserDataに値が入っていた場合にその情報を表示するJSX表現が書いてあるのですが、この場合userData.userundefinedになるため、nameageにはアクセスできずエラーが発生します。

最後にコンポーネントを利用しましょう。

./App.tsx
import React from "react";
import ErrorBoundary from "./components/ErrorBoundary";
import SampleComponent from "./components/SampleComponent";

function App() {
  return (
    <ErrorBoundary>
      <SampleComponent />
    </ErrorBoundary>
  );
}

export default App;

ErrorBoundariesコンポーネントをエラー発生の可能性のあるコンポーネントの親要素として配置します。

アプリケーションを実行するとエラー発生を知らせる『エラーが発生しました』というUIとconsoleにエラー情報がログ出力されることが確認できます。

▲DevToolsでエラー情報を確認

注意点

npm run start

create-react-appなどでアプリケーションを作成した場合、npm run startで起動させた開発用サーバーでデバッグすると、開発用サーバー側がエラーを取得してスタックトレース情報を表示してしまいます。

▲開発用サーバーがエラーのスタックトレースを表示

開発用サーバーが取得したトレースはESCキーで消すことができます。

実際のアプリケーションの動作を確認したい場合には一旦アプリケーションをビルドして確認してみるのがいいでしょう。

ターミナル
$ npm install --save-dev serve
$ npm run build
$ npx serve -s build

キャッチできないエラー

公式ドキュメントにもあるとおり、ErrorBoundariesではすべてのエラーをキャッチできるわけではありません。

サーバー側のレンダリングで発生するエラーErrorBoundaries自体で発生するエラーについてキャッチできないことはなんとなく想像できると思います。

注意すべきは非同期処理イベントハンドラーで発生したエラーについても受け取れないという点でしょう。

実際、Webアプリケーションの多くは非同期処理とイベントハンドラーを多用します。

ErrorBoundariesで取得できないエラーがそれなりに多そうですね…。

実践で作成したSampleComponent.tsxの取得できないエラーを投げる2つのバージョンについて下にサンプルをおいておきます。

興味がある方は試してみてください!!

./components/SampleComponents.tsx
import React from "react";

/**
 * ユーザーデータの構造定義
 */
interface UserData {
  user: {
    name: string;
    age: number;
  };
}

/**
 * APIの呼び出しのMock
 */
const callAPIMock = async (): Promise<UserData> => {
  const sampleResponse = '{"username": "marsquai", "age": 120}';
  throw Error("Error in callAPIMock"); // エラー処理を追加
  return JSON.parse(sampleResponse);
};

/**
 * サンプルコンポーネント
 * ユーザーデータを取得して情報を表示する
 *
 */
const SampleComponent = () => {
  const [userData, setUserData] = React.useState<UserData | undefined>(
    undefined
  );
  callAPIMock().then((data) => {
    setUserData(data);
  });
  if (userData) {
    // 想定しているデータと違う構造であるため必ずエラーが発生する
    return (
      <div>
        <div>{userData.user.name}</div>
        <div>{userData.user.age}</div>
      </div>
    );
  } else {
    return <div>データ取得中</div>;
  }
};

export default SampleComponent;
./components/SampleComponents.tsx
import React from "react";

/**
 * サンプルコンポーネント
 * ユーザーデータを取得して情報を表示する
 *
 */
const SampleComponent = () => {
  return (
    <div>
      <button
        onClick={() => {
          throw Error("Error from button");
        }}
      >
        エラー発生
      </button>
    </div>
  );
};

export default SampleComponent;

render()メソッドについて

ErrorBoundariesコンポーネントは子要素で発生したエラーをそこで止めてしまいます。そのためrender()メソッドはエラーの発生時に必ず子孫要素をレンダリングしないような分岐にしなければいけません。

では絶対に実装してはいけない例を見てみましょう。

分岐が正しく設計されていないrender()メソッド
render() {
    return (
      <>
        {this.state.hasError && <div>エラーが発生しました。</div>}
        <div>{this.props.children}</div>
      </>
    );
  }

この場合、レンダリングの処理は以下のようになります。

子孫要素でエラー発生 → stateの更新 → stateの更新により再レンダリング → 子孫要素でエラー発生 → stateの更新 → stateの更新により…

処理で無限ループが発生すると、ブラウザのメモリが無限に使われ強制終了されるか、またはブラウザがビジー状態になり逆にブラウザが閉じれないという異常事態が発生してしまいます。

絶対にこのような実装は避けましょう。

このエントリーをはてなブックマークに追加


関連記事

記事へのコメント