🌀

React: アプリ内のエラーを全部まとめて処理する(react-error-boundary)

2022/02/08に公開

はじめに

前回は、「Error Boudary」を使用したエラー処理方法について調べました。
React: アプリ内のエラーを処理する(Error Boundary)
このError Boudaryコンポーネントは、クラスコンポーネントで定義する必要があり、関数コンポーネントとして作成できません。
また、イベントハンドラでのエラーや、非同期処理でのエラーが処理できません。
そこで、コンポーネントでのエラーやイベントハンドラでのエラー、非同期処理でのエラー、全部まとめて簡単に処理できるError Boudaryのラッパー「react-error-boundary」について調べてみました。

環境

  • Windows11
  • vite: v2.7.2
  • node: v16.13.2
  • react: v17.0.2
  • typescript: v4.4.4
  • react-error-boundary: v3.1.4

react-error-boundary

react-error-boundaryはfacebook社のReact Js Core TeamのBrian Vaughnさんが作ったError Boudaryのラッパーです。
コンポーネントのレンダリングエラーやイベントハンドラ内でのエラー、非同期処理でのエラーも簡単に処理することができます。
https://github.com/bvaughn/react-error-boundary

インストール方法

以下のコマンドでインストールします。

npm install react-error-boundary

エラーが発生していないサンプルプログラムを作成する

まずは エラーが発生していないサンプルプログラムを作成します。

ソースコードを表示
main.tsx
import React from 'react'
import ReactDOM from 'react-dom'
import App from './components/App'

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
)
components/App.tsx
import Page1 from './Page1'
import Page2 from './Page2'
import Page3 from './Page3'

function App() {
  return (
    <>
      <Page1 />
      <Page2 />
      <Page3 />
    </>
  )
}
export default App
components/Page1.tsx
function Page1() {
  return (
    <div style={{ backgroundColor: '#31C2CF' }}>
      <h3>Page1</h3>
    </div>
  )
}
export default Page1
components/Page2.tsx
function Page2() {
  return (
    <div style={{ backgroundColor: '#D9A0A4' }}>
      <h3>Page2</h3>
    </div>
  )
}
export default Page2
components/Page3.tsx
import Page3Child from './Page3Child'

function Page3() {
  return (
    <div style={{ backgroundColor: '#DFD35F' }}>
      <h3>Page3</h3>
      <Page3Child />
    </div>
  )
}
export default Page3
components/Page3Child.tsx
function Page3Child() {
  const title = 'Page3Child'

  return (
    <div style={{ backgroundColor: '#DEB331' }}>
      <h5>{title}</h5>
    </div>
  )
}
export default Page3Child

基本的な使い方

まずはエラー発生時に表示するエラーコンポーネントを作成します。
エラー情報はpropsで渡されます。

components/ErrorFallback.tsx
import { FallbackProps } from 'react-error-boundary'

function ErrorFallback({ error }: FallbackProps) {
  return (
    <div>
      <h2>エラーが発生しました。</h2>
      <pre>{error.message}</pre>
    </div>
  )
}
export default ErrorFallback

エラーが発生するようPage3Childコンポーネントのコードを変更します。

components/Page3Child.tsx
import React from 'react'

function ThrowError():JSX.Element {
  throw new Error ('Page3Child Throw Error')
}

function Page3Child() {
  return (
    <div style={{ backgroundColor: '#DEB331' }}>
      <h5>Page3Child</h5>
      <ThrowError />
    </div>
  )
}
export default Page3Child

ErrorBoundaryコンポーネントで、Page3コンポーネントのPage3Childコンポーネントを囲みます。
FallbackComponentに先ほど作成したエラーページのコンポーネントを指定します。

components/Page3.tsx
import { ErrorBoundary } from 'react-error-boundary'
import ErrorFallback from './ErrorFallback'
import Page3Child from './Page3Child'

function Page3() {
  return (
    <div style={{ backgroundColor: '#DFD35F' }}>
      <h3>Page3</h3>
      <ErrorBoundary FallbackComponent={ErrorFallback}>
        <Page3Child />
      </ErrorBoundary>
    </div>
  )
}
export default Page3

Page3Childコンポーネント部分に、フォールバックUIが表示されました。

ErrorBoundaryコンポーネントで囲む範囲を変えてみる

ErrorBoundaryコンポーネントで囲む範囲を変えてみます。

Page3コンポーネントで使用していたErrorBoundaryコンポーネントは削除して、AppコンポーネントでPage3コンポーネントをErrorBoundaryコンポーネントで囲んでみます。

components/App.tsx
import { ErrorBoundary } from 'react-error-boundary'
import ErrorFallback from './ErrorFallback'
import Page1 from './Page1'
import Page2 from './Page2'
import Page3 from './Page3'

function App() {
  return (
    <>
      <Page1 />
      <Page2 />
      <ErrorBoundary FallbackComponent={ErrorFallback}>
        <Page3 />
      </ErrorBoundary>
    </>
  )
}
export default App

Page3部分にフォールバックUIが表示されました。

ErrorBoundaryコンポーネントで全てのコンポーネントを囲むようにしてみます。

components/App.tsx
function App() {
  return (
    <ErrorBoundary FallbackComponent={ErrorFallback}>
      <Page1 />
      <Page2 />
      <Page3 />
    </ErrorBoundary>
  )
}
export default App

ページ全体にフォールバックUIが表示されました。

エラー時にログを出力する

エラー時にログを出力するには、onErrorにログを出力するコールバック関数を指定します。

components/App.tsx
import { ErrorBoundary } from 'react-error-boundary'
import ErrorFallback from './ErrorFallback'
import Page1 from './Page1'
import Page2 from './Page2'
import Page3 from './Page3'

const onError = (error: Error, info: { componentStack: string }) => {
  // ここでログ出力などを行う
  console.log('error.message', error.message)
  console.log('info.componentStack:', info.componentStack)
}
function App() {
  return (
    <ErrorBoundary FallbackComponent={ErrorFallback} onError={onError}>
      <Page1 />
      <Page2 />
      <Page3 />
    </ErrorBoundary>
  )
}
export default App

エラーリカバリー(時間が解決する場合)

エラーが発生した場合、ユーザーが「再実行」できるようにできます。
エラー発生時に表示するエラーコンポーネントに再実行ボタンを設けます。
再実行ボタンのクリック時に、propsのresetErrorBoundaryに指定されているコールバック関数を実行するようにしておきます。

components/ErrorFallback.tsx
import { FallbackProps } from 'react-error-boundary'

function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
  return (
    <div>
      <h2>エラーが発生しました。</h2>
      <pre>{error.message}</pre>
      <button type="button" onClick={resetErrorBoundary}>
        もう一度、実行する
      </button>
    </div>
  )
}
export default ErrorFallback

Page3Childを少し変更し、つねにエラーが出る状態ではなく、高頻度でエラーが出る状態にします。
この場合は、時間がたてばエラーは解消されるので、再実行するためのリセット処理は不要です。

components/Page3Child.tsx
import { ErrorBoundary } from 'react-error-boundary'
import ErrorFallback from './ErrorFallback'

function onError(error: Error, info: { componentStack: string }) {
  console.log('error.message', error.message)
  console.log('info.componentStack:', info.componentStack)
}

function ThrowError(): JSX.Element {
  if (new Date().getMilliseconds() % 2 === 0 ) {
    throw new Error('Page3Child Throw Error')
  }
  return <p>not throw error</p>
}

function Page3Child() {
  return (
    <ErrorBoundary FallbackComponent={ErrorFallback} onError={onError}>
      <div style={{ backgroundColor: '#DEB331' }}>
        <h5>Page3Child</h5>
        <ThrowError />
      </div>
    </ErrorBoundary>
  )
}
export default Page3Child

実行結果です。

エラーリカバリー(状態を回復する場合)

次にエラーから回復するために、エラー状態を解消する必要がある場合です。
onResetにエラー状態を回復するコールバック関数を指定します。
resetKeysにはエラー状態を回復する為に必要な値の依存配列を指定します。
resetKeysに指定した値に変更がないかぎり、再レンダリングは行われません。

エラー発生時に表示するエラーコンポーネントは先ほどと変わりません。

components/ErrorFallback.tsx
import { FallbackProps } from 'react-error-boundary'

function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
  return (
    <div>
      <h2>エラーが発生しました。</h2>
      <pre>{error.message}</pre>
      <button type="button" onClick={resetErrorBoundary}>
        もう一度、実行する
      </button>
    </div>
  )
}
export default ErrorFallback

Page3Childを少し変更し、ボタンをクリックするとコンポーネントのレンダリングでエラーが発生するようにします。
エラー発生時にリトライできるようにonReset()でエラー状態を解消し、resetKeysを指定しています。

components/Page3Child.tsx
import React from 'react'
import { ErrorBoundary } from 'react-error-boundary'
import ErrorFallback from './ErrorFallback'

function onError(error: Error, info: { componentStack: string }) {
  console.log('error.message', error.message)
  console.log('info.componentStack:', info.componentStack)
}

function ThrowError(): JSX.Element {
  throw new Error('Page3Child Throw Error')
}

function Page3Child() {
  const [throwError, setThrowError] = React.useState(false)

  const onClick = () => {
    setThrowError((preThrowError) => !preThrowError)
  }

  const onReset = () => {
    setThrowError((pre) => !pre)
  }

  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onError={onError}
      onReset={onReset}
      resetKeys={[throwError]}
    >
      <div style={{ backgroundColor: '#DEB331' }}>
        <h5>Page4</h5>
        <button type="button" onClick={onClick}>
          Throw Error
        </button>
        {throwError && <ThrowError />}
      </div>
    </ErrorBoundary>
  )
}
export default Page3Child

イベントハンドラで発生したエラーを処理する

次にイベントハンドラで発生したエラーを処理する方法です。
新たにPage4コンポーネントを作成します。
ボタンをクリックすると例外をthrowするようにします。
try~catchでuseErrorHandlerフックが返すhandleError関数を実行します。

components/Page4.tsx
import { useErrorHandler } from 'react-error-boundary'

function Page4() {
  const handleError = useErrorHandler()

  const onClick = () => {
    try {
      throw Error('Page4 throw error') // エラーを発生させる
    } catch (error: any) {
      handleError(error)
    }
  }

  return (
    <div style={{ backgroundColor: '#FF7272' }}>
      <h3>Page4</h3>
      <button type="button" onClick={onClick}>
        Button
      </button>
    </div>
  )
}
export default Page4

イベントハンドラ内で発生した例外は、Error Boundaryでキャッチされるまで、上位のコンポーネントへ伝播します。
ここではAppコンポーネント内で、Page4コンポーネントをError Boudaryで囲んでいます。
Page4のイベントハンドラ内で発生した例外は、AppコンポーネントのError Boudaryで処理されます。

components/App.tsx
import { ErrorBoundary } from 'react-error-boundary'
import ErrorFallback from './ErrorFallback'
import Page1 from './Page1'
import Page2 from './Page2'
import Page3 from './Page3'
import Page4 from './Page4'

const onError = (error: Error, info: { componentStack: string }) => {
  // ここでログ出力などを行う
  console.log('error.message', error.message)
  console.log('info.componentStack:', info.componentStack)
}
function App() {
  return (
    <ErrorBoundary FallbackComponent={ErrorFallback} onError={onError}>
      <Page1 />
      <Page2 />
      <Page3 />
      <Page4 />
    </ErrorBoundary>
  )
}
export default App

非同期処理で発生したエラーを処理する方法です。

最後に非同期処理で発生したエラーを処理します。
こちらは先ほどのイベントハンドラーで発生したエラーを処理する方法と同じです。

新たにPage5コンポーネントを作成します。
ボタンを押すと、非同期処理でユーザー名を取得します。
非同期処理でエラーが発生した場合は、useErrorHandlerフックが返すhandleError関数を実行します。

components/Page5.tsx
import { useState } from 'react'
import { useErrorHandler } from 'react-error-boundary'

const fetchUserAPI = async (): Promise<string> => {
  const username = 'piyoko'
  const ss = new Date().getMilliseconds()
  if (ss % 2 === 0) {
    throw Error('Error in fetchUserAPI') // エラーを発生させる
  }
  return username
}

function Page5() {
  const [userName, setUserName] = useState('')
  const handleError = useErrorHandler()

  const onClick = () => {
    fetchUserAPI()
      .then((res) => {
        setUserName(res)
      })
      .catch((err) => {
        handleError(err)
      })
  }

  return (
    <div style={{ backgroundColor: '#8CCDB0' }}>
      <h3>Page4</h3>
      <button type="button" onClick={onClick}>
        Button
      </button>
      <p>ユーザー名:{userName}</p>
    </div>
  )
}
export default Page5

非同期処理内で発生したエラーは、Error Boundaryでキャッチされるまで、上位のコンポーネントへ伝播します。
ここではAppコンポーネント内で、Page5コンポーネントをError Boudaryで囲んでいます。
Page5の非同期処理内で発生したエラーは、AppコンポーネントのError Boudaryで処理されます。

components/App.tsx
import { ErrorBoundary } from 'react-error-boundary'
import ErrorFallback from './ErrorFallback'
import Page1 from './Page1'
import Page2 from './Page2'
import Page3 from './Page3'
import Page4 from './Page4'

const onError = (error: Error, info: { componentStack: string }) => {
  // ここでログ出力などを行う
  console.log('error.message', error.message)
  console.log('info.componentStack:', info.componentStack)
}
function App() {
  return (
    <ErrorBoundary FallbackComponent={ErrorFallback} onError={onError}>
      <Page1 />
      <Page2 />
      <Page3 />
      <Page4 />
      <Page5 />
    </ErrorBoundary>
  )
}
export default App

まとめ

コンポーネントでのエラーやイベントハンドラでのエラー、非同期処理でのエラー、全部まとめて簡単に処理できました。

Discussion