React で超絶楽にエラー画面を出せるよ

こんにちは。[ぬこすけ](https://twitter.com/nuko_suke_dev)です。

皆さんは React でエラー画面を出す時どうしていますか？
おそらく、こんな感じで頑張っているんじゃないかと思います。

```react
function MyWidget() {
  const [isError, setIsError] = useState(false);

  useEffect(() => {
    // 何かしらデータを取得する処理
    fetchSomething()
      .then(() => {
        // データの取得に成功した時の処理
      })
      .catch(() => {
        setIsError(true);
      })
  }, [])

  if (isError) {
    return <div>エラーです</div>
  }
  return <div>正常です</div>
}
```

いたって普通のコードです。
が、コンポーネントごとでこのようなエラーのための処理を書いていくのは手間がかかります。

場合によっては「エラーが起きたらサーバーにエラー情報を送信する」とか増えるとさらに面倒です。

今回は **React でもっと楽にエラー画面出せる方法を共有したい** と思います！

## Error Boundary って知っていますか？

React には **Error Boundary** という機能があります。

https://ja.reactjs.org/docs/error-boundaries.html

>error boundary は自身の子コンポーネントツリーで発生した JavaScript エラーをキャッチし、エラーを記録し、クラッシュしたコンポーネントツリーの代わりにフォールバック用の UI を表示する React コンポーネントです。

Error Boundary とは React 公式ドキュメントの引用文通りです。
コンポーネントでエラーが発生した時に、エラーを補足したり、エラー用の UI を表示できるというわけです。

具体的なコードを見てみましょう。
React の公式サイトのコード例から引用します。

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

  static getDerivedStateFromError(error) {
    // Update state so the next render will show the fallback UI.
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // You can also log the error to an error reporting service
    logErrorToMyService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children; 
  }
}
```

このような `ErrorBoundary` というクラスコンポーネントがあります。
`constructor` と `render` の箇所を見ると、どうやらこのコンポーネントはエラーかどうかの状態を保持していて、エラーの場合はエラー用の UI を表示するコンポーネントのようです。

普通のコンポーネントとは違って、 `getDerivedStateFromError` と `componentDidCatch` というメソッドがあります。これは一体なんでしょうか？

```react
  static getDerivedStateFromError(error) {
    // Update state so the next render will show the fallback UI.
    return { hasError: true };
  }
```

**`getDerivedStateFromError` は子孫コンポーネントで何かエラーが起きた時に呼び出されます** 。
React 公式ドキュメントのコード例では、子孫コンポーネントで何かエラーが起きた時に `{ hasError: true }` と状態を更新し、エラー用の UI を表示しています。

```react
  componentDidCatch(error, errorInfo) {
    // You can also log the error to an error reporting service
    logErrorToMyService(error, errorInfo);
  }
```

`componentDidCatch` も `getDerivedStateFromError` と同じく **子孫コンポーネントで何かエラーが起きた時に呼び出されます**。
`getDerivedStateFromError` との違いは次の 2 点です。

1. エラーの情報が取得できる
2. 副作用を実行できる

「1. エラーの情報が取得できる」については、 React 公式ドキュメントのコード例を見ると、 `componentDidCatch(error, errorInfo)` というコードが見られます。
引数に渡された `error` と `errorInfo` がエラーの情報です。

「2. 副作用を実行できる」については、普段関数コンポーネントで使っている `useEffect` と同じく外部とのデータのやりとり等の副作用を実行できます。
React 公式ドキュメントのコード例では `logErrorToMyService(error, errorInfo)` というようにサーバーへエラー情報を送信しています。

`getDerivedStateFromError` と `componentDidCatch` については次の React 公式ドキュメントに詳しく書かれています。

https://ja.reactjs.org/docs/react-component.html#static-getderivedstatefromerror

この `ErrorBoundary` の使い方ですが、次のようになります。

```react
<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>
```

普通のコンポーネントと同じ使い方です。
`getDerivedStateFromError` や `componentDidCatch` の話をふまえると、  `MyWidget` コンポーネント内(子孫コンポーネントを含む)でエラーが起きた場合は、サーバーにエラー情報が送信され、エラー用の UI が表示されるということです。

:::note warn
記事の執筆時点で、 Error Boundary はクラスコンポーネントでのみ実装できます。
関数コンポーネントでは実装できません。
:::

## 非同期コードでは使えません！！

Error Boundary の話をふまえて、冒頭で例に挙げたコードを次のようにエラーに関する処理を一掃しました。

```jsx
function MyWidget() {
  useEffect(() => {
    // 何かしらデータを取得する処理
    fetchSomething()
      .then(() => {
        // データの取得に成功した時の処理
      });
  }, [])

  return <div>正常です</div>
}

// ErrorBoundary コンポーネントで囲って使う！！
<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>
```

このコードには問題があります。
**エラーが起きた時にエラー画面が表示されません**。

![pose_english_wow_man.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164159/8fa8914b-27cc-2722-e984-94632d4648e6.png)

Error Boundary では一部エラーをキャッチできないケースがあります。
その 1 つに **非同期コード** が挙げられます。

コード例に挙げた `useEffect` 内のデータを取得する `fetchSomething` は非同期コードです。
このまま実行すると `fetchSomething` でエラーが起きても `<div>正常です</div>` が表示されます。

ではどうしたら良いのでしょうか？

![boy_question.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164159/851f9cf6-99bd-bb6b-8f2f-6fcf66b18c48.png)

答えは **`unhandledrejection` イベントを補足** することで解決します。

`unhandledrejection` イベントは `Promise` のエラーをキャッチしない場合に起こります。
具体例を見てみましょう。

```js
    // 何かしらデータを取得する処理
    fetchSomething()
      .then(() => {
        // データの取得に成功した時の処理
      });
```

この `fetchSomething` ではエラーが起こった時に `catch` 句がなく、エラーが放置されています。
この時、 `unhandledrejection` イベントが起きます。

`unhandledrejection` はイベントなので `addEventListener` で捕捉することができます。

```js
window.addEventListener("unhandledrejection", () => {
  console.log('Promise のエラーが放置されてるかもよ！！');
});
```

このコードを利用すれば、 `ErrorBoundary` は次のように書き換えられます。
（簡略化のため、一部処理やコメントアウトは削除しています）

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

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  updateError() {
    this.setState({ hasError: true });
  }

  componentDidMount() {
    window.addEventListener('unhandledrejection', this.eventHandler)
  }

  componentWillUnmount() {
    window.removeEventListener('unhandledrejection', this.eventHandler)
  }

  render() {
    if (this.state.hasError) {
      return <h1>エラーです</h1>;
    }
    return this.props.children; 
  }
}
```

この **`ErrorBoundary` コンポーネントを使うことで `fetch` のようなデータ取得でエラーになった場合もエラーの UI を表示することができます**。

多くのアプリケーションは `fetch` によるデータ取得が多いと思いますが、これで **超絶楽にエラー画面を出せるようになりました** 🥳。

```react
// ErrorBoundary で囲うだけ！！
// 各コンポーネントでエラーハンドリングは不要!!
<ErrorBoundary>
  <UserProfile userId={userId} />
  <UserArticles userId={userId} />
  <UserFollowers userId={userId} />
</ErrorBoundary>
```

:::note info
最終的なコード例として次のサイトに掲載しました。

https://playcode.io/1006344

実際に非同期コードでエラーになった場合、エラー画面が表示されていることをこのサイトで確認することができます。
:::

## 余談

余談なので読み飛ばしてもらって OK です。

今回は非同期コードの例でしたが、 React の Error Boundary でエラーを捕捉できないケースは他にもあります。
`react-error-boundary` というライブラリではこの問題を解決しているようなので、紹介しておきます。

https://github.com/bvaughn/react-error-boundary

（記事の執筆時点で最後にリリースされたのが 2021年10月なのでメンテナンスされているか怪しいですが）

もし他に良いライブラリなど知っていればコメントいただけると嬉しいです！

## まとめ

React の Error Boundary と `unhandledrejection` イベントの捕捉を組み合わせることで、コンポーネントごとでエラーハンドリングしてエラーの UI を表示を不要にさせる方法を紹介しました。

今後もフロントエンド周りの情報を発信する予定なので、よければ [ぬこすけ](https://twitter.com/nuko_suke_dev) のフォローよろしくお願いします！