11
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ReactAdvent Calendar 2023

Day 18

SWRを使いたいんじゃ!!

Posted at

こちらは React Advent Calendar 2023 18日目の記事です。

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

SWR とは

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

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

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

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

SWR の基本的な使い方

インストール

# npm
npm i swr

# yarn
yarn add swr

# pnpm
pnpm add swr

SWR でデータを取得する

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

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 からデータを取得することができます。

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 について

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

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

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

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

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

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

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

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

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

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 が判断しリクエストが実行されません。変更頻度が低いデータの取得時に使えそうですね。

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

エラーハンドリング

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

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

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

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

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

Suspense 対応

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

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 以外のリクエストを行う

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

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

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 で返します。ボタンの連打のような意図しない再実行を防げるので便利ですね。

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

最後に

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

11
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?