🐳

SWRをプロダクションコードで使うときのメモ(認証とかエラーハンドリングとか)

2021/05/22に公開

はじめに

SWR経営管理SaaSログラスの状態管理にがっつり使い始めて早4ヶ月。
実際にどのような感じで使っているかコード付きで解説します。

https://zenn.dev/yuitosato/articles/9db2a0fe90313e

前提読者

ざっくりSWRの基本的なことは抑えていることを前提にしています。

SWRのバージョン

  "swr": "0.5.6",

認証

useSWR をラップしたカスタムフックを作り、そこで認証しています。 useFetch とかいう名前にしてます。
下記はJWT方式のAuth0を利用している例です。

const useFetch = <T extends {}>(
  key: string | null,
  // 認証用のヘッダーを受け取れる非同期関数を受け取る
  fetcher: (requestHeaders: AuthReqestHeaders) => Promise<T>
): SWRResponse<T, Error> => {
  // アクセストークンを取得するためのカスタムフックを使用する
  const { getAuthReqestHeaders } = useAuthReqestHeaders();
  return useSWR(
    key,
    async () => {
      const reqOptions = await getAuthReqOptions();
      return await fetcher(reqOptions);
    }
  );
}

// 認証用のコード
type AuthReqestHeaders = {
  headers: {
    authorization: string;
  };
};

const useAuthReqestHeaders = () => {
  const { getIdTokenClaims, getAccessTokenSilently } = useAuth0();

  const getAuthReqestHeaders = async () => {
    const token = await getAccessTokenSilently();

    return {
      headers: {
        authorization: `Bearer ${token}`,
      },
    };
  };

  return { getAuthReqestHeaders };
};

// 使うとき
const useTodos = (): SWRResponse<Todo[], Error> => {
  return useFetch(
    'api/todos',
    headers => axios.get<Todo[]>('/api/todos', headers)).then(response => response.data);
    // axios使っていますが適宜読み替えてください。
}

const TodoListContainer = () => {
  const {data: todos} = useTodos();
  
  return <ul>
    {todos.map(todo => 
      <li>todo.content</li>
    )}
  </ul>  
}

ディレクトリ構成

Redux Style Guideに記載されているものに近い形のディレクトリ構成にしています。
ページをまたぐSWRは共通のディレクトリに、またがないものはコンポーネントの近くに置きます。
useSWRや上述のuseFetchをコンポーネント上でそのまま呼び出すことはなく、必ず別関数に切り出して使用します。

- src
  - common
    - hooks
      - use-fetch.ts // useSWRのラッパー関数
      - use-global-hoge-resources.ts // ページをまたいで呼び出すSWR
    - features 
      - todos
        - TodoListContainer
	- use-todos.ts // 特定のページでしか使わないものはコンポーネントの近くに置く

エラーハンドリング

ログラスではAPIコールのエラーハンドリングは中央集権的にやっています。
useHandleHttpError というカスタムフックを作り、エラーを監視させることでトーストの表示やSentryへの通知を行います。

const useFetch = <T>(
  key: string | null,
  fetcher: (requestHeaders: AuthReqestHeaders) => Promise<T>
): SWRResponse<T, Error> => {
  const { getAuthReqestHeaders } = useAuthReqestHeaders();
  const { handleHttpError } = useHandleHttpError();

  const result = useSWR(
    key,
    async () => {
      const reqOptions = await getAuthReqOptions();
      return await fetcher(reqOptions);
    }
  );
  
  useEffect(() => {
    handleHttpError(result.error);
  }, [result.error, handleHttpError]);
  
  return result;
}

テスト

コンポーネント単位でテストすることが多いですが、カスタムフック単体でテストするときは react-hooks-testing-libraryを使っています。

import { act, renderHook } from '@testing-library/react-hooks';

describe('use-todos', () => {
  it('...', () => {
   const { result, rerender } = renderHook(() => useTodos())
   rerender(); // 2回フックを評価する
   
   expect(result.data).toEqual(...);
  });
});

フォームの値が変わったらすぐSWRのコールをさせるパターン

検索フォームなど選択肢を切り替えたらするにSWRのコールをさせるときは Conditional Fetching を使いましょう。

useSWRのキーの値にフォームの値を依存させます。下記はreact-hook-formを使った例。制御コンポーネントを使う場合はuseStateなどに置き換えてみてください。

フォームの値をキーに依存させることがポイントで、useEffectなどでrevalidate関数を引数を変えて呼び出すようなパターンよりもシンプルです。

type TodoStatus = 'DONE' | 'WIP'

type TodoSearchForm = {
  status: TodoStatus
}

const useTodos = (formValue: TodoSearchForm) => {
  // フォームの値をキーに依存させます。
  // 冪等性のある文字列にできれば何でも大丈夫だが、よくJSON.stringifyを使っています。
  return useFetch(`/api/todos/${JSON.stringify(request)}`), //中略//);
}

const TodoListContainer = () => {
  const { watch } = useForm({
    defaultValues: {
      status: 'WIP'
    }
  });
  
  const { data: todos } = useTodos(watch());
 
  return <ul>
    {todos.map(todo => 
      <li>todo.content</li>
    )}
  </ul>  
}

URLのIDを参照するパターン

同じくConditional Fetchingを使いましょう。下記はNext.jsを使った例です。

const useTodos = (useId: string) => {
  // userIdをキーに依存させます。
  return useFetch(`/api/todos/${userId}`), //中略//);
}

const TodoListContainer = () => {
  const router = useRouter();
  const userId = router.current.userId as string; // キャストしないといけない、、
  
  const { data: todos } = useTodos(userId);
 
  return <ul>
    {todos.map(todo => 
      <li>todo.content</li>
    )}
  </ul>  
}

SWRを使いたい方はログラスへ!

SWRかなりいいです。
シンプルなダッシュボード系のWebアプリは基本、APIで取得するデータが状態管理の9割を占めるのでSWRはかなり相性がいいです。

SWRを使ってスイスイコードを書きたい方はぜひログラスへ。

https://job.loglass.jp/frontend

Discussion