React Error Boundaryについてまとめてみた

## はじめに
こんにちは！
[Habitat Hub](https://qiita.com/organizations/habitatHub)で活動している @kazukiiii です。
最近React Error Boundaryを用いたエラーハンドリングの実装について調べる機会があったので、今回はそこで得た知見などを復習がてら記事としてアウトプットしようかと思います！

## React Error Boundary とは
React Error Boundaryは、Reactアプリケーションにおいて、子コンポーネントツリーで発生したJavaScriptのエラーを検知し、それらをログに記録したり、ユーザーにエラーメッセージを表示するための仕組みです。

これにより、エラーが発生したコンポーネントツリーの部分だけをフォールバックUIに置き換えることができ、アプリケーション全体のクラッシュを防ぐことができます。
Error BoundaryはcomponentDidCatchやstatic getDerivedStateFromErrorといったライフサイクルメソッドを使用して定義されます。

公式ドキュメントでは、次のようなコード例が示されています。
```javascript
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; 
  }
}
```

## React Error Boundaryを利用する具体的なケース
React Error Boundaryは、特に大規模なアプリケーションやユーザーインターフェイスの複雑な部分で有効です。
例えば、外部APIからデータを取得するコンポーネント（MyComponent）を使用する場合は下記のようにコンポーネントをErrorBoundaryでラップすることでMyComponent配下にある全てのコンポーネントのエラーを補足することができます。
```javascript
<ErrorBoundary>
  <MyComponent />
</ErrorBoundary>

```
ここでMyComponent内でAPIからのレスポンスに問題があり、ヌルポなどのエラーが発生した場合、Error Boundaryによって検知され、ユーザーに対してフォールバックUIを表示するといった処理が実行可能になります。

## React Error Boundaryができないこと
React Error Boundaryは、イベントハンドラー、非同期コード（例えばsetTimeoutやrequestAnimationFrame）、サーバーサイドレンダリング、エラー境界自体の外で発生したエラーを検知することはできません。
またReact Error Boundaryについてはクラスコンポーネントでの作成が前提となっているため、関数コンポーネントを利用した実装は行うことができません。

これらの制限に対処するために、次の章で説明するreact-error-boundaryというパッケージが有用です。

## react-error-boundaryとは
react-error-boundaryは、React Error Boundaryの機能を拡張し、いくつかの制限に対処することのできるパッケージになります。

React Error Boundaryが対応できない以下の項目について、react-error-boundaryがどのように解決するかを詳しく見ていきましょう。

### イベントハンドラー
React Error Boundaryは、イベントハンドラー内で発生したエラーを検知できません。
ですが、react-error-boundaryを使用すると、イベントハンドラー内で発生したエラーをErrorBoundaryコンポーネントのonErrorプロパティを通じて検知し、適切なエラー処理を行うことが可能になります。

### 非同期コード
非同期コード（例えばsetTimeoutやPromise）内で発生するエラーも、標準のError Boundaryでは検知できません。
react-error-boundaryでは、非同期処理を包むコンポーネントをError Boundaryでラップすることで、非同期処理中に発生したエラーを検知し、適切に処理できます。

### サーバーサイドレンダリング
サーバーサイドレンダリング中に発生したエラーはクライアントサイドのError Boundaryでは検知できません。
react-error-boundaryは主にクライアントサイドでの使用を目的としており、サーバーサイドでの直接的な対応は提供していません。
サーバーサイドでのエラーをハンドリングするには別途検討が必要となります。

### エラー境界自体の外で発生したエラー
react-error-boundaryは、Error Boundaryの外側で発生したエラー（例えばルートコンポーネントのエラー）に対しては直接対処できませんが、アプリケーションの最上位にError Boundaryを配置することで、より広範囲のエラーを検知しやすくなります。

## react-error-boundaryの書き方
react-error-boundaryのErrorBoundaryコンポーネントを使用する際にはPropsとしてさまざまなデータを渡すことができます。

### 5-1. FallbackComponent
FallbackComponentプロパティでは、エラー発生時に表示されるコンポーネントを定義します。このコンポーネントはエラー情報を受け取り、カスタマイズされたエラーメッセージを表示できます。
```javascript
function MyFallbackComponent({error, resetErrorBoundary}) {
  return (
    <div role="alert">
      <p>An error occurred: {error.message}</p>
      <button onClick={resetErrorBoundary}>Try again</button>
    </div>
  );
}

<ErrorBoundary FallbackComponent={MyFallbackComponent}>
  <MyComponent />
</ErrorBoundary>
```
ここでは、MyFallbackComponentはエラーが発生した場合に表示されるUIを定義しており、エラーメッセージとリトライボタンを含んでいます。

### 5-2. onError
onErrorプロパティは、エラーが発生したときに実行される関数を指定します。この関数はエラー情報を受け取り、ログサービスへの送信などの処理を行います。
```javascript
function handleError(error, errorInfo) {
  // エラー情報をログサービスに送信
  logErrorToMyService(error, errorInfo);
}

<ErrorBoundary onError={handleError}>
  <MyComponent />
</ErrorBoundary>
```

### 5-3. onReset
onResetプロパティは、エラー状態のリセットを行う関数を指定します。この関数は、ユーザーがエラーUI上で何らかのアクションを行ったときに呼び出されます。
```javascript
<ErrorBoundary
  onReset={() => {
    // リセット処理
    resetMyComponentState();
  }}
  FallbackComponent={MyFallbackComponent}
  onError={handleError}
>
  <MyComponent />
</ErrorBoundary>
```
onResetで指定された関数は、エラー状態をリセットするために使用されます。ここでは、resetMyComponentState関数を呼び出しています。

### 5-4. resetKeys
resetKeysプロパティは、特定のキーの値が変わると自動的にエラー状態をリセットするための依存配列を指定します。
```javascript
const [user, setUser] = useState(null);

<ErrorBoundary
  resetKeys={[user]}
  FallbackComponent={MyFallbackComponent}
  onError={handleError}
  onReset={() => setUser(null)}
>
  <MyComponent user={user} />
</ErrorBoundary>
```
この例では、userステートが変更された際に、ErrorBoundaryのエラー状態がリセットされます。

これらのPropsを組み合わせることで、react-error-boundaryを用いたエラーハンドリングをより効果的に行うことができます。

## 最後に
React Error Boundaryを利用することで、アプリケーションで発生するエラーについて補足し、デフォルトのエラー画面の代わりに運用者側で予め用意したエラー画面の表示や処理の実行などを行うことができます。

複雑なシステムになる程予期せぬエラーが発生するものなので、こうしたハンドリング処理はしっかり導入したいものですね。