こんにちは。ぬこすけです。
皆さんは 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
というクラスコンポーネントがあります。
constructor
と render
の箇所を見ると、どうやらこのコンポーネントはエラーかどうかの状態を保持していて、エラーの場合はエラー用の UI を表示するコンポーネントのようです。
普通のコンポーネントとは違って、 getDerivedStateFromError
と componentDidCatch
というメソッドがあります。これは一体なんでしょうか?
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);
}
componentDidCatch
も getDerivedStateFromError
と同じく 子孫コンポーネントで何かエラーが起きた時に呼び出されます。
getDerivedStateFromError
との違いは次の 2 点です。
- エラーの情報が取得できる
- 副作用を実行できる
「1. エラーの情報が取得できる」については、 React 公式ドキュメントのコード例を見ると、 componentDidCatch(error, errorInfo)
というコードが見られます。
引数に渡された error
と errorInfo
がエラーの情報です。
「2. 副作用を実行できる」については、普段関数コンポーネントで使っている useEffect
と同じく外部とのデータのやりとり等の副作用を実行できます。
React 公式ドキュメントのコード例では logErrorToMyService(error, errorInfo)
というようにサーバーへエラー情報を送信しています。
getDerivedStateFromError
と componentDidCatch
については次の React 公式ドキュメントに詳しく書かれています。
この ErrorBoundary
の使い方ですが、次のようになります。
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>
普通のコンポーネントと同じ使い方です。
getDerivedStateFromError
や componentDidCatch
の話をふまえると、 MyWidget
コンポーネント内(子孫コンポーネントを含む)でエラーが起きた場合は、サーバーにエラー情報が送信され、エラー用の UI が表示されるということです。
記事の執筆時点で、 Error Boundary はクラスコンポーネントでのみ実装できます。
関数コンポーネントでは実装できません。
非同期コードでは使えません!!
Error Boundary の話をふまえて、冒頭で例に挙げたコードを次のようにエラーに関する処理を一掃しました。
function MyWidget() {
useEffect(() => {
// 何かしらデータを取得する処理
fetchSomething()
.then(() => {
// データの取得に成功した時の処理
});
}, [])
return <div>正常です</div>
}
// ErrorBoundary コンポーネントで囲って使う!!
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>
このコードには問題があります。
エラーが起きた時にエラー画面が表示されません。
Error Boundary では一部エラーをキャッチできないケースがあります。
その 1 つに 非同期コード が挙げられます。
コード例に挙げた useEffect
内のデータを取得する fetchSomething
は非同期コードです。
このまま実行すると fetchSomething
でエラーが起きても <div>正常です</div>
が表示されます。
ではどうしたら良いのでしょうか?
答えは 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>
最終的なコード例として次のサイトに掲載しました。
実際に非同期コードでエラーになった場合、エラー画面が表示されていることをこのサイトで確認することができます。
余談
余談なので読み飛ばしてもらって OK です。
今回は非同期コードの例でしたが、 React の Error Boundary でエラーを捕捉できないケースは他にもあります。
react-error-boundary
というライブラリではこの問題を解決しているようなので、紹介しておきます。
(記事の執筆時点で最後にリリースされたのが 2021年10月なのでメンテナンスされているか怪しいですが)
もし他に良いライブラリなど知っていればコメントいただけると嬉しいです!
まとめ
React の Error Boundary と unhandledrejection
イベントの捕捉を組み合わせることで、コンポーネントごとでエラーハンドリングしてエラーの UI を表示を不要にさせる方法を紹介しました。
今後もフロントエンド周りの情報を発信する予定なので、よければ ぬこすけ のフォローよろしくお願いします!