[React] SWRでエラー処理を共通化する

SWRのエラーを共通化する方法についてまとめます。

Reactプロジェクトがあるのを前提とします。

SWRConfigでエラー処理の共通化をする方法


SWRConfigでSWRについてのグローバルな設定ができます。

https://swr.vercel.app/ja/docs/global-configuration

その中にあるonErrorでエラー発生時の処理を書くことができます。

https://swr.vercel.app/ja/docs/error-handling#%E3%82%B0%E3%83%AD%E3%83%BC%E3%83%90%E3%83%AB%E3%82%A8%E3%83%A9%E3%83%BC%E3%81%AE%E5%A0%B1%E5%91%8A

以下のようなイメージです。今回はfetcherにkyを使っている前提で書いています。

400ならページ上部にスナックバー表示、それ以外ならErrorFallbackに任せるみたいな流れにするとSWRのhookを使った時にほとんどエラー処理する必要がなくなります。

import { HTTPError } from "ky";
import { SWRConfig } from "swr";

const App = () => {
    <SWRConfig
      value={{
        onError: async (err) => {
          if (err instanceof HTTPError) {
            const res = await err.response.clone();
            if (res.status === 400) {
              // snackbarでエラー表示など
              return;
            }
          }
          // ErrorFallbackなどその他のエラー処理
          handleError(err);
        },
      }}
    >
    ...

サンプルコード


では、実際にSWRConfigを使ったエラー共通化のサンプルコードを書いていきます。

400エラーが起きたらsnackbarでエラー表示、その他のエラーはErrorBoundary/ErrorFallbackで共通処理するサンプルです。

ちなみに完成したコードはこちらに上げています。

https://github.com/pei223/ReactSWRErrorHandling

ライブラリインストール

まずは必要なライブラリのインストール。kyはAPIアクセス、notistackはスナックバー表示、react-error-boundaryはErrorBoundary関連のライブラリです。

npm i swr ky notistack react-error-boundary

SWRConfigのエラー処理の仕組み周り

まずは起点となるApp.tsx。

TestAppの方でSnackbarProvider、ErrorBoundaryの設定をします。これで以下のコンポーネントでスナックバー表示、ErrorBoundaryへの処理の移譲ができます。

次にAppRouter。本来ここにReact-routerのルーティングなど書くのでAppRouterという命名です。

ここでSWRConfigのonErrorでエラー処理を書いています。400エラーならスナックバー表示、それ以外ならErrorFallbackに処理を移譲する感じです。

import React from "react";
import { ErrorBoundary, useErrorHandler } from "react-error-boundary";
import { SnackbarProvider, useSnackbar } from "notistack";
import { SWRConfig } from "swr";
import TestComponent from "./components/TestComponent";
import TestErrorFallback from "./TestErrorFallback";
import { HTTPError } from "ky";

const AppRouter = () => {
  const handleError = useErrorHandler();
  const { enqueueSnackbar } = useSnackbar();
  return (
    <SWRConfig
      value={{
        onError: async (err) => {
          if (err instanceof HTTPError) {
            const res = await err.response.clone();
            if (res.status === 400) {
              enqueueSnackbar(`BadRequest: ${err.message}`, {
                variant: "error",
              });
              return;
            }
          }
          handleError(err);
        },
      }}
    >
      {/** ここにReact routerの設定など */}
      <TestComponent />
    </SWRConfig>
  );
};

function TestApp() {
  return (
    <SnackbarProvider
      maxSnack={5}
      anchorOrigin={{ horizontal: "center", vertical: "top" }}
      autoHideDuration={3000}
    >
      <ErrorBoundary FallbackComponent={TestErrorFallback}>
        <AppRouter />
      </ErrorBoundary>
    </SnackbarProvider>
  );
}

export default TestApp;

次はErrorFallback。エラーメッセージだけ表示してますが、ちゃんとやるなら401は再ログイン促したりエラーレスポンス取得してその内容表示とかしたほうがいいです。

import React from "react";
import { FallbackProps } from "react-error-boundary";

const TestErrorFallback: React.FC<FallbackProps> = ({
  error,
  resetErrorBoundary,
}) => {
  return (
    <div>
      <h1>Error</h1>
      <p>message: {error.message}</p>
      <button onClick={resetErrorBoundary}>Try again</button>
    </div>
  );
};

export default TestErrorFallback;

最後にコンポーネント実装

最後にそれらの仕組みが整った上でコンポーネントの実装。

useSWRでデータ取得処理書くだけでエラーが発生したらSWRConfig/ErrorFallbackがやってくれます。

import React from "react";
import ky from "ky";
import useSWR from "swr";

type TodoList = {
  items: Todo[];
  total: number;
};

type Todo = {
  id: string;
  title: string;
  done: boolean;
};
const fetcher = (url: string): Promise<TodoList> => {
  return ky.get(url).json();
};

const TestComponent = () => {
  const { data, error } = useSWR<TodoList>(["http://localhost:8080/todos"], fetcher);
  const loading = !error && !data;
  return (
    <div>
      {loading && <div>Loading...</div>}
      {data &&
        data.items.map((todo) => (
          <div
            key={todo.id}
            style={{
              borderTop: "1px solid gray",
            }}
          >
            <p>id: {todo.id}</p>
            <p>title: {todo.title}</p>
            <p>status: {todo.done ? "finished" : "not finished"}</p>
          </div>
        ))}
    </div>
  );
};

export default TestComponent;

コメントを残す

メールアドレスが公開されることはありません。