SWRをプロダクションコードで使うときのメモ(認証とかエラーハンドリングとか)
はじめに
SWRを経営管理SaaSログラスの状態管理にがっつり使い始めて早4ヶ月。
実際にどのような感じで使っているかコード付きで解説します。
前提読者
ざっくり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を使ってスイスイコードを書きたい方はぜひログラスへ。
Discussion