React: アプリ内のエラーを全部まとめて処理する(react-error-boundary)
はじめに
前回は、「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のラッパーです。
コンポーネントのレンダリングエラーやイベントハンドラ内でのエラー、非同期処理でのエラーも簡単に処理することができます。
インストール方法
以下のコマンドでインストールします。
npm install react-error-boundary
エラーが発生していないサンプルプログラムを作成する
まずは エラーが発生していないサンプルプログラムを作成します。
ソースコードを表示
import React from 'react'
import ReactDOM from 'react-dom'
import App from './components/App'
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
)
import Page1 from './Page1'
import Page2 from './Page2'
import Page3 from './Page3'
function App() {
return (
<>
<Page1 />
<Page2 />
<Page3 />
</>
)
}
export default App
function Page1() {
return (
<div style={{ backgroundColor: '#31C2CF' }}>
<h3>Page1</h3>
</div>
)
}
export default Page1
function Page2() {
return (
<div style={{ backgroundColor: '#D9A0A4' }}>
<h3>Page2</h3>
</div>
)
}
export default Page2
import Page3Child from './Page3Child'
function Page3() {
return (
<div style={{ backgroundColor: '#DFD35F' }}>
<h3>Page3</h3>
<Page3Child />
</div>
)
}
export default Page3
function Page3Child() {
const title = 'Page3Child'
return (
<div style={{ backgroundColor: '#DEB331' }}>
<h5>{title}</h5>
</div>
)
}
export default Page3Child
基本的な使い方
まずはエラー発生時に表示するエラーコンポーネントを作成します。
エラー情報はpropsで渡されます。
import { FallbackProps } from 'react-error-boundary'
function ErrorFallback({ error }: FallbackProps) {
return (
<div>
<h2>エラーが発生しました。</h2>
<pre>{error.message}</pre>
</div>
)
}
export default ErrorFallback
エラーが発生するようPage3Childコンポーネントのコードを変更します。
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
に先ほど作成したエラーページのコンポーネントを指定します。
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コンポーネントで囲んでみます。
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コンポーネントで全てのコンポーネントを囲むようにしてみます。
function App() {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Page1 />
<Page2 />
<Page3 />
</ErrorBoundary>
)
}
export default App
ページ全体にフォールバックUIが表示されました。
エラー時にログを出力する
エラー時にログを出力するには、onError
にログを出力するコールバック関数を指定します。
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
に指定されているコールバック関数を実行するようにしておきます。
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を少し変更し、つねにエラーが出る状態ではなく、高頻度でエラーが出る状態にします。
この場合は、時間がたてばエラーは解消されるので、再実行するためのリセット処理は不要です。
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
に指定した値に変更がないかぎり、再レンダリングは行われません。
エラー発生時に表示するエラーコンポーネントは先ほどと変わりません。
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を指定しています。
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関数を実行します。
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で処理されます。
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関数を実行します。
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で処理されます。
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