7
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Code ChrysalisAdvent Calendar 2019

Day 15

React で side effect のエラーハンドリング

Posted at

この記事は CodeChrysalis Advent Calendar 2019 の記事です。

はじめに

ReactjsにはError Boundariesという、エラーをcatchしたときに専用のComponentをrenderしてくれる機能がありますが、これはrender時におけるエラーのみcatchする機能です。

componentDidCatchというライフサイクルが用意されていて、これを以てサードパーティのエラー監視パッケージに通知するようにと推奨されています。

ですが多くの場合、フロントエンドはバックエンドと通信する等のside effectを持ちます。

  1. side effectを用いたときのエラーハンドリングが必要となる
  2. side effectを用いたときもエラーメッセージを表示するComponentを共通化したい
  3. 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で十中八九バックエンドか通信周りが関係していると悟ります。

さいごに

もしより良いエラーハンドリングの構造があればぜひ教えてください!

7
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?