投稿日:2021年2月11日
ErrorBoundariesとは子孫要素で発生したエラーの処理をするために作成されたコンポーネントです。エラー発生したことをユーザーに知らせたり、エラーログを作成したりするために使用します。この記事ではErrorBoundariesコンポーネントの使い方・ユースケース・使用の際の注意点について解説しています。
ErrorBoundariesとは子孫要素で発生したエラーの処理をするために作成されたコンポーネントです。エラー発生したことをユーザーに知らせたり、エラーログを作成したりするために使用します。
ErrorBoundariesはあくまでReactのクラスコンポーネントの機能をつかって自分で実装するコンポーネントであることを覚えておきましょう。
この記事のコードは以下の環境で確認されました。
以下のような条件を満たすコンポーネントとErrorBoundariesと呼びます。
実際にErrorBoundariesコンポーネントから作成しましょう。コンポーネント名はわかりやすくErrorBoundaryにしておきます。
import React, { ErrorInfo } from "react";
/**
* 子要素でエラーが発生した際に取得し、その情報をコンソールログに出力する
* エラーが発生したことはユーザーに知らせる
*/
class ErrorBoundary extends React.Component<{}, { hasError: boolean }> {
constructor(props: {}) {
super(props);
this.state = {
hasError: false,
};
}
static getDerivedStateFromError(): { hasError: boolean } {
console.log("getDerivedStatefromErrorがよばれました。");
return { hasError: true };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
console.log(error);
console.log(errorInfo);
}
render() {
if (this.state.hasError) {
return <div>エラーが発生しました</div>;
}
return <>{this.props.children}</>;
}
}
export default ErrorBoundary;
getDerivedStateFromError()メソッドはその名の通りエラーをもとにstateに値をセットするためのメソッドです。返り値はstateの更新値のオブジェクトです。おそらく、ライフサイクル側で返し値のオブジェクトでsetState()が呼ばれているのだと思います。ユーザーに対してエラーが発生していることを知らせるためのstate更新をするのがいいでしょう。
componentDidCatch()はエラー発生時にエラー情報を引数として呼ばれます。第1引数には発生したError、第2引数には発生したエラー情報のオブジェクトが渡されます。サイト管理者が利用するログの作成、エラーの発生を知らせるために使用しましょう。
render()メソッド内で、this.state.hasErrorの値を条件として返すReactノードを分岐させていることに注目しましょう。this.state.hasErrorのbool値は初期値がfalseでgetDerivedStateFromError()メソッドが呼ばれた際に、trueにセットされるように記述しました。そのため、一度でも子孫要素のレンダリングでエラーが発生するとエラーの発生を知らせる要素の描画に分岐されます。
次に、意図的にエラーを発生させるサンプルのコンポーネントを作成します。
import React from "react";
/**
* ユーザーデータの構造定義
*/
interface UserData {
user: {
name: string;
age: number;
};
}
/**
* APIの呼び出しのMock
*/
const callAPIMock = async (): Promise<UserData> => {
const sampleResponse = '{"username": "marsquai", "age": 120}';
return JSON.parse(sampleResponse);
};
/**
* サンプルコンポーネント
* ユーザーデータを取得して情報を表示する
*
*/
const SampleComponent = () => {
const [userData, setUserData] = React.useState<UserData | undefined>(
undefined
);
callAPIMock().then((data) => {
setUserData(data);
});
if (userData) {
// 想定しているデータと違う構造であるため必ずエラーが発生する
return (
<div>
<div>{userData.user.name}</div>
<div>{userData.user.age}</div>
</div>
);
} else {
return <div>データ取得中</div>;
}
};
export default SampleComponent;
ここではまずcallAPIMock()というAPIの呼び出しを模倣する非同期関数を作成しました。この関数の中でJSONの文字列をパースしているのですが、肝心のJSON文字列が返し値の型と一致していないというミスがあります。これが今回のエラーの発生原因になります。
SampleComponentというコンポーネントではcallAPIMock()関数を呼び出し、結果が返された際にuserDataという変数にデータを渡します。
userData変数はUserDataインターフェイスの型を想定しているのですが、callAPIMock()の処理はミスによりUserDataと違う型のオブジェクトが返されます。render()メソッドではuserDataに値が入っていた場合にその情報を表示するJSX表現が書いてあるのですが、この場合userData.userはundefinedになるため、nameとageにはアクセスできずエラーが発生します。
最後にコンポーネントを利用しましょう。
import React from "react";
import ErrorBoundary from "./components/ErrorBoundary";
import SampleComponent from "./components/SampleComponent";
function App() {
return (
<ErrorBoundary>
<SampleComponent />
</ErrorBoundary>
);
}
export default App;
ErrorBoundariesコンポーネントをエラー発生の可能性のあるコンポーネントの親要素として配置します。
アプリケーションを実行するとエラー発生を知らせる『エラーが発生しました』というUIとconsoleにエラー情報がログ出力されることが確認できます。
create-react-appなどでアプリケーションを作成した場合、npm run startで起動させた開発用サーバーでデバッグすると、開発用サーバー側がエラーを取得してスタックトレース情報を表示してしまいます。
開発用サーバーが取得したトレースはESCキーで消すことができます。
実際のアプリケーションの動作を確認したい場合には一旦アプリケーションをビルドして確認してみるのがいいでしょう。
$ npm install --save-dev serve
$ npm run build
$ npx serve -s build
公式ドキュメントにもあるとおり、ErrorBoundariesではすべてのエラーをキャッチできるわけではありません。
サーバー側のレンダリングで発生するエラーやErrorBoundaries自体で発生するエラーについてキャッチできないことはなんとなく想像できると思います。
注意すべきは非同期処理やイベントハンドラーで発生したエラーについても受け取れないという点でしょう。
実際、Webアプリケーションの多くは非同期処理とイベントハンドラーを多用します。
ErrorBoundariesで取得できないエラーがそれなりに多そうですね…。
実践で作成したSampleComponent.tsxの取得できないエラーを投げる2つのバージョンについて下にサンプルをおいておきます。
興味がある方は試してみてください!!
import React from "react";
/**
* ユーザーデータの構造定義
*/
interface UserData {
user: {
name: string;
age: number;
};
}
/**
* APIの呼び出しのMock
*/
const callAPIMock = async (): Promise<UserData> => {
const sampleResponse = '{"username": "marsquai", "age": 120}';
throw Error("Error in callAPIMock"); // エラー処理を追加
return JSON.parse(sampleResponse);
};
/**
* サンプルコンポーネント
* ユーザーデータを取得して情報を表示する
*
*/
const SampleComponent = () => {
const [userData, setUserData] = React.useState<UserData | undefined>(
undefined
);
callAPIMock().then((data) => {
setUserData(data);
});
if (userData) {
// 想定しているデータと違う構造であるため必ずエラーが発生する
return (
<div>
<div>{userData.user.name}</div>
<div>{userData.user.age}</div>
</div>
);
} else {
return <div>データ取得中</div>;
}
};
export default SampleComponent;
import React from "react";
/**
* サンプルコンポーネント
* ユーザーデータを取得して情報を表示する
*
*/
const SampleComponent = () => {
return (
<div>
<button
onClick={() => {
throw Error("Error from button");
}}
>
エラー発生
</button>
</div>
);
};
export default SampleComponent;
ErrorBoundariesコンポーネントは子要素で発生したエラーをそこで止めてしまいます。そのためrender()メソッドはエラーの発生時に必ず子孫要素をレンダリングしないような分岐にしなければいけません。
では絶対に実装してはいけない例を見てみましょう。
render() {
return (
<>
{this.state.hasError && <div>エラーが発生しました。</div>}
<div>{this.props.children}</div>
</>
);
}
この場合、レンダリングの処理は以下のようになります。
子孫要素でエラー発生 → stateの更新 → stateの更新により再レンダリング → 子孫要素でエラー発生 → stateの更新 → stateの更新により…
処理で無限ループが発生すると、ブラウザのメモリが無限に使われ強制終了されるか、またはブラウザがビジー状態になり逆にブラウザが閉じれないという異常事態が発生してしまいます。
絶対にこのような実装は避けましょう。