SWRを使いたいんじゃ!!

こちらは [React Advent Calendar 2023](https://qiita.com/advent-calendar/2023/react) 18日目の記事です。

皆さんは普段 React 開発で API からのデータ取得のために何を使われているでしょうか？
axios, fetch, TanStack Query など様々な選択肢があると思いますが、その中でも今回は [SWR](https://swr.vercel.app/ja) に焦点を当てて簡単に紹介したいと思います。

## SWR とは

https://swr.vercel.app/ja

SWR とは Next.js でおなじみの Vercel 社が提供しているデータ取得のためのライブラリです。今や React に無くてはならない React Hooks ベースの API で使うことができます。

ちなみに SWR という名前は以下から来ています。

>“SWR” という名前は、 HTTP RFC 5861(opens in a new tab) で提唱された HTTP キャッシュ無効化戦略である stale-while-revalidate に由来しています。 SWR は、まずキャッシュからデータを返し（stale）、次にフェッチリクエストを送り（revalidate）、最後に最新のデータを持ってくるという戦略です。
> 引用元: [データ取得のための React Hooks ライブラリ SWR](https://swr.vercel.app/ja)

SWR の特徴としては以下が挙げられます。

- 高速かつ軽量
- キャッシュによる再利用可能なデータ取得や重複したリクエストを排除できる
- SSR/ISR/SSG をサポートしている
- TypeScript に対応
- React Hooks で API が提供されているため宣言的にデータ取得を記述できる
- React 推奨ではないが Suspense に対応している

それでは実際の使い方を見てみましょう。

## SWR の基本的な使い方

### インストール

```sh
# npm
npm i swr

# yarn
yarn add swr

# pnpm
pnpm add swr
```

### SWR でデータを取得する

https://swr.vercel.app/ja/docs/data-fetching

ここでは以下の API を例に説明していきます。

https://digimon-api.com/

```tsx
import useSWR from 'swr';

interface Content {
  id: number;
  name: string;
  href: string;
  image: string;
}

interface Pageable {
  currentPage: number;
  elementsOnPage: number;
  totalElements: number;
  totalPages: number;
  previousPage: string;
  nextPage: string;
}

interface Response {
  content: Content[];
  pageable: Pageable;
}

const fetcher: Fetcher<Response, string> = (url) =>
  fetch(url).then((res) => res.json());

function App() {
  const { data, error } = useSWR<Response>(
    'https://digi-api.com/api/v1/digimon',
    fetcher
  );

  if (error) return <div>デジモンに出会えなかったみたい...</div>;
  if (!data) return <div>読み込み中...</div>;

  return (
    <ul>
      {data.content.map((digimon) => {
        return <li key={digimon.id}>{digimon.name}</li>;
      })}
    </ul>
  );
}

export default App;
```

useSWR を使って API からデータを取得することができます。

```tsx
const fetcher: Fetcher<Response, string> = (request: RequestInfo) =>
  fetch(request).then((res) => res.json());
...
const { data, error } = useSWR<Response>(
  'https://digi-api.com/api/v1/digimon',
  fetcher
);
```

第一引数に API の URL を、第二引数に実際に API リクエストを行いデータを取得する関数を渡します。
この第一引数は URL を渡すことが多いですが、key として扱われ、これを基に SWR が取得したデータをキャッシュします。
第二引数には、公式サイトでもこの例でも fetch を使っていますが、axios など任意の API クライアントを使用することができます。`Fetcher` という SWR 提供の型定義を見ていくと分かりますが、Promise を返す関数であれば渡すことができます。

### key について

https://swr.vercel.app/ja/docs/arguments

デフォルトで SWR はグローバルキャッシュを使用してすべてのコンポーネント間でデータを共有することができます。このデータを区別するために使用するのが先程出てきた key です。
そのため異なるデータリソースやデータを表す key はユニークにする必要があります。基本的には API の URL で問題ない場合があるとは思いますが、以下のように URL 内に含まれないデータ(ここではトークン)によって結果が変わるという場合もあります。この場合はトークンが変わっても key は同一のため、トークンが変わる前のデータを返すことになります。

```tsx
useSWR('https://api.github.com/repos/OWNER/REPO/issues', url => fetchWithToken(url, token));
```

この場合は、key を配列とし、token を配列に含めることで url と token の組み合わせが key となるため、トークンが変更した場合も新しいデータとして保存し、共有することができます。

```tsx
useSWR(['https://api.github.com/repos/OWNER/REPO/issues', token], url => fetchWithToken(url, token));
```

### 取得したデータを更新する

https://swr.vercel.app/ja/docs/revalidation#revalidate-on-focus

上記ページに記載の通り、ページにフォーカスを合わせるかタブを切り替えると SWR が自動的にデータを再検証し、更新します。
デフォルトで有効なため、無効化したい場合は `revalidateOnFocus` を `false` にしましょう。

また、オプションの `refreshInterval` を設定することで、指定した時間ごとに定期的な自動更新を行うこともできます。

```tsx
const { data, error } = useSWR<Response>(
  'https://digi-api.com/api/v1/digimon',
  fetcher,
  { refreshInterval: 1000 }
);
```

ここまで自動の方式をいくつか紹介しましたが、手動で更新したい場合は返り値の `mutate` を使用します。

```tsx
const { data, error, mutate } = useSWR<Response>(
  'https://digi-api.com/api/v1/digimon',
  fetcher
);
...
return (
...
    <button
      onClick={() => {
        mutate('https://digi-api.com/api/v1/digimon')
      }}
    >
      Update User
    </button>
  );
...
```

### 重複したリクエストを排除する

`dedupingInterval` オプションを設定することでそのミリ秒の間は再レンダリングや画面のリロードで再リクエストを行おうとしても重複したリクエストとして SWR が判断しリクエストが実行されません。変更頻度が低いデータの取得時に使えそうですね。

```tsx
const { data, error } = useSWR<Response>(
  'https://digi-api.com/api/v1/digimon',
  fetcher,
  { dedupingInterval: 60 * 1000 }
);
```

### エラーハンドリング

API リクエストでエラーが起きた場合は、返り値の `error` にエラーオブジェクトがセットされます。このオブジェクトを使ってエラーハンドリングを行うことができます。

```tsx
const { data, error } = useSWR<Response>(
  'https://digi-api.com/api/v1/digimon',
  fetcher
);
```

また、Sentry にエラーを送信したいなど共通したエラーハンドリングを行いたい場合、`SWRConfig` コンテキストですべての SWR フックに設定することもできます。

```tsx
import * as Sentry from "@sentry/browser";

...
<SWRConfig value={{
  onError: (error, key) => {
    if (error.status !== 403 && error.status !== 404) {
      Sentry.captureException(error)
    }
  }
}}>
  <App />
</SWRConfig>
```

### Suspense 対応

https://swr.vercel.app/ja/docs/suspense

`suspense` オプションを有効化することで、Suspense を使用することができます。お手軽ですね。

```tsx
import { ErrorBoundary } from "react-error-boundary";

function DigimonList() {
  const { data } = useSWR<Response>(
    'https://digi-api.com/api/v1/digimon',
    fetcher,
    { suspense: true }
  );

  return (
    <ul>
      {data.content.map((digimon) => {
        return <li key={digimon.id}>{digimon.name}</li>;
      })}
    </ul>
  );
}

function App() {
  return (
    <ErrorBoundary fallback={<div>デジモンに出会えなかったみたい...</div>}>
      <Suspense fallback={<div>読み込み中...</div>}>
        <DigimonList />
      </Suspense>
    </ErrorBoundary>
  )
}
```

## ちなみに... SWR で GET 以外のリクエストを行う

https://swr.vercel.app/ja/docs/mutation#useswrmutation

データ取得を主軸としたライブラリですが、POST リクエストなどデータの作成/更新などを行う場合にも対応しています。この場合 `useSWR` の代わりに `useSWRMutation` を使います。

ここでは以下の API を例に取り上げます。

https://docs.github.com/ja/rest/issues/issues?apiVersion=2022-11-28#create-an-issue

```tsx
import useSWRMutation from 'swr/mutation';

interface Issue {
  title: string;
  body: string;
}

const token = 'xxxxxxx';

async function createIssue(url: string, { arg }: { arg: Issue }) {
  const { title, body } = arg;
  await fetch(url, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${token}`,
      'X-GitHub-Api-Version': '2022-11-28',
      Accept: 'application/vnd.github+json',
    },
    body: JSON.stringify({
      title,
      body,
    }),
  });
}

function App() {
  // OWNER, REPO は issue の作成先を指定します。
  const { trigger, isMutating } = useSWRMutation('https://api.github.com/repos/OWNER/REPO/issues', createIssue);

  return (
    <button
      disabled={isMutating}
      onClick={async () => {
        try {
          await trigger({
            title: 'test issue',
            body: 'test body',
          });
        } catch (e) {
          console.error(e);
          alert('issue 作成に失敗しました。');
        }
      }}
    >
      Update User
    </button>
  );
}

export default App;
```

`useSWR` と似た形ですが、`useSWRMutation` では第一引数に key (主にリクエスト先の URL)を指定し、第二引数に POST リクエストを行う関数を渡します。PUT や PATCH, DELETE の場合も useSWRMutation で同様に扱うことができます。
実行は返り値の `trigger` 関数を使用して任意のタイミングで行います。実行後に API の結果が必要な場合は `useSWR` のように返り値の data で取得することもできます。
`isMutating` は返り値の一つで、実行中かどうかを `boolean` で返します。ボタンの連打のような意図しない再実行を防げるので便利ですね。

```tsx
const { trigger, isMutating } = useSWRMutation('https://api.github.com/repos/OWNER/REPO/issues', createIssue);
```

## 最後に

私も使い始めたレベルなので、簡単にですが、使い方について記載しました。
サーバーデータをキャッシュできるのでグローバルの状態管理がグッと減らせる、Suspense に対応できるのでより宣言的にコンポーネントを実装できるなど良さを実感しているので、既に使っている方も多いかもしれませんがぜひぜひ使っていきましょう！