Error Boundary は React のv16
から導入された機能で、これを使うとコンポーネント内で発生したエラーをキャッチすることが出来る。
主に、エラー用のUIを表示したり、エラーを記録したりすることに使われる。
前者にはstatic getDerivedStateFromError
というメソッドを、後者にはcomponentDidCatch
というメソッドを用いる。
Error Boundary のためのクラスコンポーネントを作り、そこにこれらのメソッドを定義して使う。
この記事では、バージョン16.8.6
で動作確認している。
動作確認用のアプリの用意
以下の内容でアプリを作る。
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>Template htmlfile</title> </head> <body> <p>↓この下に React アプリが表示される↓</p> <div id="app"></div> <p>↑この上に React アプリが表示される↑</p> </body> </html>
import React from 'react'; import ReactDOM from 'react-dom'; const AquaChild = () => ( <div style={{backgroundColor: 'white'}}>aqua child</div> ); const Aqua = () => ( <div style={{backgroundColor: 'aqua', padding: '10px'}}> aqua <AquaChild /> </div> ); const Lime = () => <div style={{backgroundColor: 'lime'}}>lime</div>; const App = () => ( <> <Aqua /> <Lime /> </> ); ReactDOM.render(<App />, document.querySelector('#app'));
これをビルドすると以下のようなUIを持ったアプリが出来るので、これを対象に検証していく。
エラー用のUIを表示させる
React はv16
から、発生したエラーがキャッチされなかった場合、コンポーネントツリー全体をアンマウントするようになった。
公式ドキュメントによればこれは、壊れたUIを表示することは何も表示しないことよりも悪いことである、という考えによるもの。
試しに、先程作ったサンプルでエラーを発生させてみる。
const Aqua = () => ( <div style={{backgroundColor: 'aqua', padding: '10px'}}> - aqua + aqua{x} <AquaChild /> </div> );
Aqua
のなかでx
を参照しているが、x
は存在しない変数なのでReferenceError
が発生する。
この結果、アプリ全体が表示されなくなる。
エラーが発生したAqua
だけでなく、Lime
も含めたコンポーネントツリー全体がアンマウントされているのが分かる。
static getDerivedStateFromError
を使ってエラーをキャッチすることで、ツリー全体がアンマウントされるのを防ぎ、適切なUIを表示させることが出来る。
Error Boundary を使うため、以下のコンポーネントを作成した。
class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = {hasError: false}; } static getDerivedStateFromError(error) { console.log(error instanceof Error); return {hasError: true}; } render() { const {state, props} = this; if (state.hasError) { return ( <div style={{backgroundColor: 'orange'}}> 背景がオレンジのコンポーネントは ErrorBoundary によって表示されているエラー用のコンポーネントです。 </div> ); } return props.children; } }
static getDerivedStateFromError
の引数で、キャッチしたエラーオブジェクトを取得できる。
そしてこのコンポーネントで、Aqua
とLime
をそれぞれラップする。
const App = () => ( <> - <Aqua /> - <Lime /> + <ErrorBoundary> + <Aqua /> + </ErrorBoundary> + <ErrorBoundary> + <Lime /> + </ErrorBoundary> </> );
こうすることで、Aqua
やLime
のなかでエラーが発生した際に、それをキャッチできる。
この状態でページを表示すると、今度は以下のようになっている。
Aqua
で発生したエラーをキャッチしてエラー用のUIが表示されている。
その一方で、Lime
ではエラーが発生していないので、そのまま表示されている。
このように、Error Boundary を上手く使うことでアプリ全体がクラッシュするのを防ぎ、適切なUIをユーザーに提供できるようになる。
Error Boundary の直下ではなくもっと深い階層でエラーが発生しても、問題なくキャッチできる。
const AquaChild = () => ( - <div style={{backgroundColor: 'white'}}>aqua child</div> + <div style={{backgroundColor: 'white'}}>aqua child{x}</div> ); const Aqua = () => ( <div style={{backgroundColor: 'aqua', padding: '10px'}}> - aqua{x} + aqua <AquaChild /> </div> );
Aqua
ではなくAquaChild
でエラーを発生させても、先程と同じ表示になる。
コンポーネントのなかでエラーが発生すると、ツリーを上に辿っていき、一番最初に到達した ErrorBoundary がエラーをキャッチする仕組みになっている。つまり、JavaScript のcatch{}
と同じような挙動である。
最後までキャッチされなかった場合は、既に述べたようにツリー全体がアンマウントされる。
Error Boundary の対象になるのは、配下のコンポーネントで発生したエラーのみ。
Error Boundary 自身のなかで発生したエラーは、キャッチすることが出来ない。
例えば以下のようにすると、キャッチできずにツリー全体がアンマウントされてしまう。
const AquaChild = () => ( - <div style={{backgroundColor: 'white'}}>aqua child{x}</div> + <div style={{backgroundColor: 'white'}}>aqua child</div> ); const App = () => ( <> <ErrorBoundary> <Aqua /> + <div>{x}</div> </ErrorBoundary> <ErrorBoundar
このエラーをキャッチしたければ、App
をラップする必要がある。
-ReactDOM.render(<App />, document.querySelector('#app')); +ReactDOM.render( + <ErrorBoundary> + <App /> + </ErrorBoundary>, + document.querySelector('#app') +);
そうするとApp
の代わりにエラー用のUIが表示されるようになる。
エラーを記録する
エラーをログに残したり、エラー監視サービスなどに送信したりする場合は、Error Boundary にcomponentDidCatch
メソッドを定義して、そのなかで行う。
return {hasError: true}; } + componentDidCatch(error, info) { + console.log(error); + console.log(info.componentStack); + } + render() { const {state, props} =
第一引数で、キャッチしたエラーオブジェクトを取得できる。
第二引数のinfo
はcomponentStack
を持っており、ここには、エラーが発生したコンポーネントのスタックトレースが入っている。
以下は、AquaChild
でエラーが発生した際のスタックトレース。
in AquaChild (at src/index.js:13) in div (at src/index.js:11) in Aqua (at src/index.js:21) in ErrorBoundary (at src/index.js:20) in App (at src/index.js:29)
static getDerivedStateFromError
との違いだが、公式のAPIリファレンスによれば、static getDerivedStateFromError
はUIの描画のために使い、副作用を扱う場合にcomponentDidCatch
を使うとよいらしい。
キャッチできないエラー
既に述べたように Error Boundary は自身のエラーをキャッチすることが出来ないが、配下のコンポーネントのエラーでもキャッチしないものが2つある。
1つ目は、イベントハンドラ内でのエラー。
以下のようにボタンを押した際にエラーが発生するようにした場合、レンダリング時にはエラーを出さないので、そのまま表示される。
<div style={{backgroundColor: 'aqua', padding: '10px'}}> aqua <AquaChild /> + <button + type="button" + onClick={() => { + x; + }} + > + error button + </button> </div>
そしてボタンを押すとエラーが発生するのだが、このエラーはキャッチされず、表示にも影響がない。
2つ目が、非同期処理のなかでのエラー。これも、エラーはどこにもキャッチされないまま終わり、アプリはそのまま表示され続ける。
const Aqua = () => { Promise.resolve().then(() => x); return ( <div style={{backgroundColor: 'aqua', padding: '10px'}}> aqua <AquaChild /> </div> ); };