LoginSignup
125
127

More than 1 year has passed since last update.

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

Posted at

こんにちは。ぬこすけです。

皆さんは 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 という機能があります。

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

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

具体的なコードを見てみましょう。
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 というクラスコンポーネントがあります。
constructorrender の箇所を見ると、どうやらこのコンポーネントはエラーかどうかの状態を保持していて、エラーの場合はエラー用の UI を表示するコンポーネントのようです。

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

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

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

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

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

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

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

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

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

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

<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>

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

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

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

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

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

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

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

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

pose_english_wow_man.png

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

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

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

boy_question.png

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

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

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

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

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

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

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

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 によるデータ取得が多いと思いますが、これで 超絶楽にエラー画面を出せるようになりました 🥳。

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

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

https://playcode.io/1006344

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

余談

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

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

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

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

まとめ

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

今後もフロントエンド周りの情報を発信する予定なので、よければ ぬこすけ のフォローよろしくお願いします!

125
127
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
125
127