この記事は CodeChrysalis Advent Calendar 2019 の記事です。
はじめに
ReactjsにはError Boundariesという、エラーをcatchしたときに専用のComponentをrenderしてくれる機能がありますが、これはrender時におけるエラーのみcatchする機能です。
componentDidCatch
というライフサイクルが用意されていて、これを以てサードパーティのエラー監視パッケージに通知するようにと推奨されています。
ですが多くの場合、フロントエンドはバックエンドと通信する等のside effectを持ちます。
- side effectを用いたときのエラーハンドリングが必要となる
- side effectを用いたときもエラーメッセージを表示するComponentを共通化したい
- side effectを用いたときもサードパーティのエラー監視パッケージ(Sentryなど)に通知したい
という要件に対してのエラーハンドリング専用のComponentを考えました。
前提として、React Hooksを使います。
設計の内容
エラーが発生したときにContextを使ってエラーの情報を保存し、専用Componentでそれらの情報を使うようにしました。
コードの内容
Context
import React, { useState, useContext, createContext } from 'react';
const ErrorContext = createContext({
hasError: false,
userMessage: null,
error: null,
setContextError: (userMessage: string, error: Error) => {},
setCotextErrorDone: () => {},
});
export const ErrorProvider: React.FC<object> = props => {
const [hasError, setHasError] = useState<boolean>(false);
const [userMessage, setUserMessage] = useState<string | null>(null);
const [error, setError] = useState<Error | null>(null);
const setContextError = (userMessage: string, error: Error) => {
setUserMessage(userMessage);
setError(error);
setHasError(true);
};
const setCotextErrorDone = () => {
setUserMessage(null);
setError(null);
setHasError(false);
};
return (
<ErrorContext.Provider
value={{ hasError, userMessage, error, setContextError, setCotextErrorDone }}
{...props}
/>
);
};
export const useError = () => {
const context = useContext(ErrorContext);
if (context === undefined) {
throw new Error(`useError must be used within a ErrorProvider`);
}
return context;
};
要素の説明
-
hasError
はこれがtrue
になっているとErrorハンドリングのComponentをrenderするようにするためのstateです。 -
userMessage
はエラーが発生した箇所でエラー内容が特定できるものに関してはその場でこのstateにエラーメッセージを保管し、ErrorハンドリングのComponentをrenderするときに使えるようにしています。 -
error
はエラーが発生した箇所のtrycatch
で取得したerror
インスタンスそのものを入れています。Sentry
に送るためのものです。 -
setContextError
でエラー発生時にエラー内容を保存し、hasError
のstateを変更します。 -
setContextErrorDone
で、ErrorハンドリングのComponentからどこかに遷移するときにhasError
のstateをfalse
に変更するようにしています。
import React, { useState } from 'react';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import * as Sentry from '@sentry/browser';
import { Header } from 'components';
import { useUser } from 'context/UserContext';
import { useError } from 'context/ErrorContext';
import styles from './ErrorBoundary.module.css';
const ErrorBoundary: React.FC<RouteComponentProps> = ({ history: { push } }) => {
const { userId } = useUser();
const { hasError, userMessage, error, setCotextErrorDone } = useError();
const [eventId, setEventId] = useState<string | null>(null);
const handleHelp = () => {
setCotextErrorDone();
setEventId(null);
push({ pathname: '/somewhere' });
};
if (hasError && !eventId) {
Sentry.withScope(scope => {
if (attendanceId) {
scope.setUser({ id: userId });
}
scope.setExtras({ userMessage });
scope.setTag('errorCategory', 'Side Effect');
const currentEventId = Sentry.captureException(error);
setEventId(currentEventId);
});
}
let title = 'エラーが発生しました';
if (hasError) {
return (
<aside className={styles.mainContainer}>
<Header />
<h1 className={styles.mainHeader}>{title}</h1>
<h2 className={styles.subHeader}>
<span>{userMessage}</span>
</h2>
<button className={styles.inquiryButton} onClick={handleHelp}>ヘルプ</button>
</aside>
);
}
return null;
};
export default withRouter(ErrorBoundary);
要素の説明
- ErrorハンドリングのComponentを作成することで、Error時に表示するUIもカスタマイズで作成できます。
- このComponentがrenderされたらSentryにメッセージを通知するようにしています。以下のコードの部分です。
Sentry.withScope(scope => {
if (attendanceId) {
scope.setUser({ id: userId });
}
scope.setExtras({ userMessage });
scope.setTag('errorCategory', 'Side Effect');
const currentEventId = Sentry.captureException(error);
setEventId(currentEventId);
});
- ヘルプボタンを押すと
handleHelp
を実行して、setContextErrorDone
が実行され、エラーに関するContextがクリアされると同時にどこかのURIに移動するようにしています。 - 別のContextである
userUser
からuserId
を取得して、Sentryに送信し、もし問い合わせが来たらSentry上のエラーメッセージと関連付けて調査できるようにしています(Sentry便利!) -
let title = ...
の部分はもしErrorの内容によってタイトルを変えたいときのためにlet
にしています。
Sentry
Sentryの設定で、Slackに送信するように連携しておけばこれらのメッセージをSlackに送信できます。
また、
scope.setUser({ id: userId });
これは任意のユーザー属性を割り当てることができます。
scope.setExtras({ userMessage });
これは補足情報を割り当てることができます。ここではユーザーにどのような情報を表示しているのかを把握するために割り当てています。
scope.setTag('errorCategory', 'Side Effect');
Sentryの画面にタグを表示できるので、React純粋のError BoundaryとこのカスタムのBoundaryを切り分けています。前者はrenderのエラー、後者はSide Effectで十中八九バックエンドか通信周りが関係していると悟ります。
さいごに
もしより良いエラーハンドリングの構造があればぜひ教えてください!