🔥

SWRを使って型定義されたhooksを書いてみる(GET編)

2024/06/28に公開

こんにちは。
最近じめじめしていて髪の毛が爆発しています。8zkです。

先日SWRを使う機会があったので今回はそのご紹介をしたいと思います。

SWRとは

https://swr.vercel.app/ja
Vercel社が提供しているデータ取得のためのライブラリです。

特徴は下記の通りです。

  • 速い、 軽量 そして再利用可能なデータの取得
  • 組み込みのキャッシュとリクエストの重複排除
  • リアルタイムな体験
  • トランスポートとプロトコルにとらわれない
  • SSR / ISR / SSG support
  • TypeScript 対応
  • React Native 対応

採用理由

SWRを採用した理由は、プロジェクト内のデータ取得のロジックを単純化してdata, error, isLoading, mutateを提供してくれるからです。
また、キャッシュ機構を持っているため、不要なリクエストを防ぐことができます。

今回はSWRの紹介ではなく、より実践的(?)な形で利用した場合に型周りやディレクトリ構成がこんな実装になるよといった感じでSWRを紹介をしたい紹介したいと思います!
ちなみに私はNext.jsで利用しています。(適宜'use client'を追加してください)

SWRではfetcherを別に用意する必要があるので、今回はaxiosと合わせて使います。もちろんfetchでも可能です。

// /src/helpers/fetcher.ts
import axios from 'axios';

export const fetcher = axios.create({
  baseURL: 'https://jsonplaceholder.typicode.com',
  timeout: 10000,
  headers: { 'X-Custom-Header': 'foobar' },
});

axios.createを使ってaxiosのinstanceを作成します。ここではサービス全体で利用するbaseURLtimeoutを設定します。

次に作成したfetcheruseSWRを使って各endpointのhooksの元となる実装を作成します。
今回はGETのみです。

データ取得の下準備

// /src/hooks/api/useAPI.ts
import useSWR from 'swr';
import { AxiosRequestConfig, AxiosError } from 'axios';

export const useQuery = <Request extends {} = {}, Response extends {} = {}>(
  url: string,
  params?: Request,
  axiosConfig?: AxiosRequestConfig
) => {
  const key = [url, JSON.stringify(params)];

  return useSWR<Response, AxiosError>(key, () =>
    fetcher
      .request({
        url,
        method: 'GET',
        params,
        ...axiosConfig,
      })
      .then((res) => res.data)
  );
};

今回の実装のキモはkeyです。
SWRはStale While Revalidate(再検証している間は古いキャッシュを返す)の略なのでキャッシュがとても上手に設計されているライブラリです。
キャッシュは@から始まるkeyとしてSWRに保存されます。以下のようにSWRのキャッシュを確認できます。

import { useSWRConfig } from 'swr';

// 何かしらのAPI call via useSWR
const { cache } = useSWRConfig();
console.log(cache); // "@"/comments","{\"postId\":\"1\"}","

今回の実装ではkey[url, JSON.stringify(params)]な形で定義しています。

個人的にJSON.stringifyされたparamsはkeyに必須だと思います。
理由としてはGETのような振る舞いをするPOSTのケースにおいて、query parameterではなくrequest bodyとしてparameterが含まれるため、endpointにはparameterが含まれません。その場合endpointだけだと一意に結果が決まらないため、keyにparameterを含めるのが良いと思います。

またuseSWRkeyは配列で受け取ることも可能ですが、浅い比較しか行わないため必ずJSON.stringifyしてください。

defaultのmethodがGETになっていますが、useQueryの第三引数にaxiosConfigを渡しているため、よしなにmethodtimeout等の設定をendpoint毎に変更することが可能です。

データの取得

先ほど作成したuseQueryを使って型定義を行ないます。
この定義により実際に使うcomponentの中では型定義されたrequestやresponseが推論されます。
また/src/hooks/api以下にuse~.ts(usePost.tsなど)を必要に応じて増やしていくイメージです。

// /src/hooks/api/useComment.ts
import { useQuery } from '@/hooks/api/useAPI';

type FetchCommentsRequest = {
  postId: string;
};

type Comment = {
  postId: number;
  id: number;
  name: string;
  email: string;
  body: number;
};

type FetchCommentsResponse = Comment[];

export const useFetchComments = (params: FetchCommentsRequest) => {
  return useQuery<FetchCommentsRequest, FetchCommentsResponse>(
    '/comments',
    params
  );
};

componentでは以下のように利用します。
useSWRを利用しているためdataだけでなくerrorisLoadingをhooksから利用できます。これもサービスに応じてハンドリングしてください。

// /src/routes/Home/index.tsx
import Error from 'next/error';
import { useFetchComments } from '@/hooks/api/useComment';

export const Home = () => {
  const {
    data: comments,
    error: fetchCommentsError, 
    isLoading: isFetchCommentsLoading,
  } = useFetchComments({ postId: '1' });

  if (isFetchCommentsLoading) {
    return <p>Loading...</p>;
  }

  if (fetchCommentsError || !comments) {
    return <Error statusCode={400} />;
  }

  return (
    <div>
      <h1>Home</h1>
      {comments.map((comment) => (
        <div key={comment.id}>{comment.body}</div>
      ))}
    </div>
  );
};

このようにcomponent側で型定義されたSWRのhooksを利用できるようになりました。

今回SWRを使ってみて、データ取得のロジックを単純化して型定義されたhooksを利用にできるようになりとても便利だと思いました!
次回はPOST, PUT, DElETE編です。

最後に

スペースマーケットでは一緒に働く仲間を募集しています!
軽く話を聞いてみたいなという方も大歓迎ですので、興味を持っていただけた方はご応募お待ちしています!

スペースマーケット Engineer Blog

Discussion