こちらは 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 が自動的にデータを再検証し、更新します。
デフォルトで有効なため、無効化したい場合は revalidateOnFocus
を false
にしましょう。
また、オプションの 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 に対応できるのでより宣言的にコンポーネントを実装できるなど良さを実感しているので、既に使っている方も多いかもしれませんがぜひぜひ使っていきましょう!