はじめに
Reactでデータフェッチング、みなさんどうしてますか。
useEffectとuseStateを組み合わせて、ローディング状態を管理して、エラーハンドリングして...正直なところ、毎回同じようなコードを書くのがしんどいんですよね。
そんな悩みを解決してくれるのが、今回紹介するSWRというライブラリです。Vercel(Next.jsを作っている会社)が開発していて、GitHubスター数は32,000超え。月間ダウンロード数は約2,500万という、かなりの人気ライブラリになっています。
個人的には、一度使い始めたらもう戻れないくらい便利だと思っています。
とはいえ、説明を読むより実際にuseSWRが動くところを見た方が早いと思います。ローディング状態の切り替わりや、ボタン操作による再検証が画面上でどう見えるかを先に確認したい方はこちらからどうぞ。
SWRとは
SWRは「stale-while-revalidate」というHTTPキャッシュ戦略に基づいたReact用のデータフェッチングライブラリです。
この戦略、何がすごいかというと:
- まずキャッシュからデータを返す(古くても即座に表示)
- バックグラウンドで新しいデータを取得
- 取得完了したら画面を更新
つまり、ユーザーには常に「何か」が表示されている状態を保ちつつ、最新データへの更新も行えるという話です。体感的なスピードがかなり上がります。
特徴・メリット
SWRの良いところを整理してみます。
軽量でシンプル
バンドルサイズは約8.5KB(gzip圧縮時)。これ、意外と重要なんですよね。データフェッチングのためだけに重いライブラリを入れたくないので。
自動キャッシュと重複排除
同じAPIを複数コンポーネントから呼んでも、実際のリクエストは1回だけ。キャッシュも自動で管理してくれます。これだけで開発がだいぶ楽になります。
自動再検証
- タブにフォーカスが戻ったとき
- ネットワークが復旧したとき
- 一定間隔(ポーリング)
こういったタイミングで勝手にデータを最新にしてくれます。リアルタイム性が求められるアプリでも使いやすい。
TypeScript対応
型推論がしっかり効くので、TypeScriptプロジェクトとの相性が良いです。30代になって型の恩恵を強く感じるようになりました。
SSR/SSG対応
Next.jsとの相性は抜群。というか同じVercelが作っているので当然ですね。サーバーサイドでのプリフェッチもスムーズに行えます。
インストール方法
インストールはnpmかyarnで一発です。
# npm
npm install swr
# yarn
yarn add swr
# pnpm
pnpm add swr
依存関係はReact 16.11.0以上のみ。シンプルで良いですね。
SWRのサンプルを動かす
まずはuseSWRを実際に動かしてみましょう。下のサンプルはJSONPlaceholder(テスト用の公開API)からTodoを1件取得して表示するもので、入力欄でIDを変えるとuseSWRのキーが変わり、自動的に再フェッチされます。「再検証」ボタンを押すとmutate()が呼ばれ、キャッシュを保持したままバックグラウンドで最新データを取り直す様子も確認できます。
要点だけ抜き出すと、こういう形になります。
import useSWR from 'swr'
const fetcher = (url: string) => fetch(url).then((res) => res.json())
function Todo({ id }: { id: number }) {
const { data, error, isLoading, mutate } = useSWR(
`https://jsonplaceholder.typicode.com/todos/${id}`,
fetcher
)
if (error) return <p>エラーが発生しました</p>
if (isLoading) return <p>読み込み中...</p>
return (
<div>
<p>{data.title}</p>
<button onClick={() => mutate()}>再検証</button>
</div>
)
}
実際に動かせるものが下です。IDを入力し直すとisLoadingがtrueに戻って読み込み中の表示に切り替わり、取得が終わるとタイトルが更新されます。
data・error・isLoadingの3つだけで表示を出し分けられるのがポイントです。IDを空にしたり範囲外の値にしたりすると、useSWRのキーが変わって即座に別のリクエストが発生するのも分かります。「再検証」ボタンのmutate()をmutate(undefined, { revalidate: true })のように書き換えれば、より細かく再検証の挙動を制御することもできます。
基本的な使い方
最小構成
まずは一番シンプルな例から。
import useSWR from 'swr'
// fetcher関数を定義
const fetcher = (url: string) => fetch(url).then(res => res.json())
function Profile() {
const { data, error, isLoading } = useSWR('/api/user', fetcher)
if (error) return <div>エラーが発生しました</div>
if (isLoading) return <div>読み込み中...</div>
return <div>こんにちは、{data.name}さん</div>
}
これだけです。useEffectもuseStateも書かなくて良い。QOL上がりますね。
グローバル設定
fetcher関数を毎回書くのは面倒なので、グローバルに設定できます。
import { SWRConfig } from 'swr'
const fetcher = (url: string) => fetch(url).then(res => res.json())
function App() {
return (
<SWRConfig value={{ fetcher }}>
<Profile />
<Dashboard />
</SWRConfig>
)
}
これで各コンポーネントではURLを渡すだけで済みます。
条件付きフェッチ
ユーザーがログインしているときだけデータを取得したい、みたいなケース。
function UserProfile({ userId }: { userId: string | null }) {
// userIdがnullのときはフェッチしない
const { data } = useSWR(userId ? `/api/users/${userId}` : null, fetcher)
if (!userId) return <div>ログインしてください</div>
if (!data) return <div>読み込み中...</div>
return <div>{data.name}</div>
}
キーにnullを渡すとフェッチをスキップしてくれます。
ミューテーション(楽観的UI)
データ更新時に、サーバーレスポンスを待たずにUIを更新する「楽観的UI」も簡単に実装できます。
import useSWR, { mutate } from 'swr'
function TodoList() {
const { data: todos } = useSWR('/api/todos', fetcher)
const addTodo = async (text: string) => {
const newTodo = { id: Date.now(), text, completed: false }
// 即座にUIを更新(楽観的更新)
mutate('/api/todos', [...todos, newTodo], false)
// 実際のAPIリクエスト
await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo)
})
// 再検証してサーバーの状態と同期
mutate('/api/todos')
}
return (
// ...
)
}
ユーザー体験がかなり良くなります。
SWRのmutateで楽観的更新を試す
mutate()はSWRのキャッシュを直接書き換える関数です。第2引数に新しいデータ、第3引数にfalseを渡すと、サーバーからの応答を待たずにその場でUIだけを更新できます(=楽観的更新)。ここではAPI通信をローカルのタイマー処理に置き換えて、ネットワーク環境に依存せず挙動を確認できるようにしています。
要点を抜き出すとこうなります。
import useSWR, { mutate } from 'swr'
const addTodo = async (todos, text) => {
const newTodo = { id: Date.now(), text }
// 通信を待たずにキャッシュを即更新
mutate('/todos', [...todos, newTodo], false)
await saveTodoToServer(newTodo) // 実際のAPIリクエスト
mutate('/todos') // サーバーの状態と再同期
}
実際に触れるサンプルが下です。テキストを入力して「追加」を押すと、mutateによって一覧が即座に更新される様子(楽観的更新)と、その約800ms後に「保存中...」の表示が消えて確定する様子の両方が見えます。
追加した項目が「送信中」の表示付きですぐ一覧に現れ、少し経つとその表示が消える、という2段階の変化がmutateの呼び出しタイミングによって作られているのが分かります。falseを渡さずにmutate(KEY, newList)とだけ書くと、SWRは即座に裏側で再検証(revalidate)を行おうとする点も違いとして押さえておくとよいでしょう。
実践的なユースケース
ページネーション
function Posts({ page }: { page: number }) {
const { data, isLoading } = useSWR(`/api/posts?page=${page}`, fetcher)
if (isLoading) return <div>読み込み中...</div>
return (
<ul>
{data.posts.map((post: Post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
ページ番号をキーに含めるだけで、ページごとにキャッシュが効きます。
無限スクロール
useSWRInfiniteを使うと無限スクロールも実装できます。
import useSWRInfinite from 'swr/infinite'
function InfiniteList() {
const getKey = (pageIndex: number, previousPageData: any) => {
if (previousPageData && !previousPageData.length) return null
return `/api/items?page=${pageIndex}`
}
const { data, size, setSize, isLoading } = useSWRInfinite(getKey, fetcher)
const items = data ? data.flat() : []
return (
<div>
{items.map((item: Item) => (
<div key={item.id}>{item.name}</div>
))}
<button onClick={() => setSize(size + 1)}>
もっと読み込む
</button>
</div>
)
}
ポーリング(定期更新)
リアルタイム性が必要な画面では、ポーリングも設定一つで実現できます。
function StockPrice() {
const { data } = useSWR('/api/stock/price', fetcher, {
refreshInterval: 3000 // 3秒ごとに更新
})
return <div>現在価格: {data?.price}円</div>
}
SWRのrefreshIntervalでポーリングを試す
useSWRの第3引数にrefreshInterval(ミリ秒)を渡すだけで、指定した間隔で自動的に再フェッチしてくれます。手動でsetIntervalを組む必要がないのが便利なところです。ここでは外部APIの代わりに、呼び出すたびに現在時刻とカウントを返すfetcherを使い、通信環境に依存せず更新間隔を体感できるようにしています。
const { data } = useSWR('poll-key', fetcher, {
refreshInterval: 2000, // 2秒ごとに再フェッチ
})
下のサンプルでは2秒ごとに時刻とカウントが自動更新されます。refreshInterval: 0にすると自動更新が止まり、再検証はフォーカス復帰などのタイミングだけになる点も合わせて確認できます。
ボタンでrefreshIntervalを0に切り替えると更新が止まり、取得回数のカウントが増えなくなります。実際の画面ではrefreshIntervalを株価やダッシュボードの更新頻度に合わせて数秒〜数十秒程度に設定するのが一般的です。
エラーリトライ
ネットワークエラー時の自動リトライも組み込まれています。
const { data } = useSWR('/api/data', fetcher, {
onErrorRetry: (error, key, config, revalidate, { retryCount }) => {
// 404はリトライしない
if (error.status === 404) return
// 最大10回まで
if (retryCount >= 10) return
// 5秒後にリトライ
setTimeout(() => revalidate({ retryCount }), 5000)
}
})
React Query(TanStack Query)との比較
よく比較されるReact Queryとの違いについても触れておきます。
| 項目 | SWR | React Query |
|---|---|---|
| バンドルサイズ | 約8.5KB | 約13KB |
| 機能の豊富さ | シンプル | 多機能 |
| 学習コスト | 低い | やや高い |
| DevTools | なし | あり |
個人的には、シンプルなプロジェクトならSWR一択ですね。React Queryは高機能だけど、その分複雑になりがちです。必要十分な機能で軽量なSWRは、多くのケースで最適解だと思います。
まとめ
SWRを使うと、Reactでのデータフェッチングがかなり楽になります。
- キャッシュ戦略が秀逸 - stale-while-revalidateで体感速度アップ
- コード量が減る - useEffect + useStateの定型コードから解放
- 自動再検証が便利 - フォーカス時やネットワーク復旧時に勝手に更新
- 軽量 - 8.5KBで必要十分な機能
- TypeScript対応 - 型推論がしっかり効く
正直なところ、新規のReactプロジェクトでSWRを使わない理由がないくらいです。まだ使ったことがない方は、ぜひ試してみてください。時短になるし、コードもスッキリしますよ。