SWR 完全ガイド:キャッシュ戦略と楽観的更新の実践テクニック
金曜日の午後3時、私は画面上で回り続けるローディングスピナーを、ユーザーリストページでN回目のリロードをしながら見つめていました。タブを切り替えるたびにローディング、他のページから戻るたびにローディング、電話に出るためにブラウザからフォーカスを外して戻ってきただけでもローディング。一番腹立たしいのは何か? そのデータは1秒前に読み込んだばかりだということです。
あなたも似たような経験があるはずです。React プロジェクトを書く時、データ取得のたびに useState、useEffect、さらに loading、error の処理ロジックを書かなければなりません。最低でも20行のコードです。さらに頭が痛いのは、複数のコンポーネントが同じデータを必要とする場合です。状態を親コンポーネントに持ち上げるか(さらにコードが増える)、各コンポーネントで個別にリクエストするか(ネットワーク資源の無駄)の二択を迫られます。
正直なところ、一時期は自分の書き方が間違っているのではないかと疑いました。その後 SWR に出会った時の「マジかよ、こんなに簡単にできるの?」という感覚は、フォルダの手動コピーを Git に置き換えた時の衝撃に近かったです。3行のコードで、問題の80%が解決しました。大袈裟ではありません。
今日は、Vercel 製の React データ取得ライブラリ「SWR」についてお話しします。核心はたった一つの言葉:stale-while-revalidate(陳腐化している間に再検証)。技術的に聞こえますか? 原理は超シンプルです。まずキャッシュされた「古い写真」を見せて(高速)、裏でこっそり新しい写真を撮り(正確)、撮り終わったらそっと差し替える(シームレス)。
なぜ SWR が必要なのか? 従来のデータ取得の3大苦痛
まず、従来の React データ取得がどのようなものか見てみましょう。ユーザーリストを表示するとします:
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch('/api/users')
.then(res => res.json())
.then(data => {
setUsers(data);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <ul>{users.map(user => <li key={user.id}>{user.name}</li>)}</ul>;
}
24行のコード。ただリストを表示するためだけに。これでも「まあ普通」だと思いますか? では、以下のシナリオを想像してください:
苦痛1:吐き気がするほどの重複コード
データを必要とするすべてのコンポーネントでこれを書く必要があります。ユーザーリスト、記事リスト、コメントリスト…。3つの状態、一連の if 判定、エラー処理…コピペするのも恥ずかしくなります。ある中規模プロジェクトで数えたら、このテンプレートコードだけで全体の20%を占めていました。
苦痛2:キャッシュ? そんなものはない
最も辛いのはキャッシュがないことです。ユーザーがトップページから詳細ページに行き、またトップページに戻ると——また長いローディングです。データは30秒前に取得したばかりなのに、再リクエストが必要です。ユーザーからの「サイトが遅い」という不満は、API が遅いからではなく、単にキャッシュしていないからです。
「グローバル状態管理を自分で書けばいいじゃん」と言う人もいます。ええ、できます。でもそうすると、Redux/Zustand の store、action、reducer をメンテナンスすることになります…。3分で終わるはずの機能が、30分のアーキテクチャ設計に変わります。
苦痛3:複数コンポーネント間のデータ同期は悪夢
さらに厄介なのが、複数のコンポーネントで同じデータを使う場合です。例えば、ヘッダーに未読メッセージ数を表示し、サイドバーにも表示し、メッセージリストページにも表示したい場合。どうしますか? 最上位まで状態を持ち上げますか? 更新のたびに十数層の props バケツリレーです。Context を使いますか? メッセージ更新のたびにツリー全体が再レンダリングされます。
以前、メッセージ通知機能を修正した際、ロジックは単純なのに状態同期の処理に2日かかりました。コミットした時、画面いっぱいの setState と useEffect を見て、自分でも目を覆いたくなりました。
これが SWR が人気な理由です。ただの車輪の再発明ではなく、本当に痛みを解決してくれるからです。SWR で上記のコードを書き直すと?
import useSWR from 'swr';
function UserList() {
const { data, error, isLoading } = useSWR('/api/users', fetcher);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error!</div>;
return <ul>{data.map(user => <li key={user.id}>{user.name}</li>)}</ul>;
}
9行。完了です。しかも自動キャッシュ、自動再検証、コンポーネント間共有付き。あ、fetcher はあなたの fetch 関数です。通常はグローバルに一度定義すれば十分です:
const fetcher = url => fetch(url).then(r => r.json());
この差は一目瞭然です。
SWR の核心概念:Stale-While-Revalidate 戦略
SWR という名前は、HTTP RFC 5861 で定義されたキャッシュ無効化戦略に由来します。「RFC」という言葉に身構えないでください。原理は超簡単です。
写真を例えにすると:友達の最近の写真が見たいとします。従来の方法は「電話して写真を撮って送ってもらうよう頼み、待つ」こと。SWR の方法は「とりあえず前回会った時に撮った古い写真(先週のかも)を見せ、見ている間にメッセージを送って新しい写真を撮らせ、届いたら差し替える」ことです。
重要な違いはどこでしょう? 待つ必要がないことです。すぐにコンテンツが見られます(少し古いかもしれませんが)。同時に、最終的には最新のものが見られることが保証されます。これが “stale-while-revalidate” —— 古いものを表示しながら、再検証する —— です。
SWR の完全なワークフロー:
-
ステップ1:キャッシュを即座に返す(stale)
- コンポーネントのマウント時、SWR はまずローカルキャッシュをチェックします。
- キャッシュがある? 即座に返します。ページは瞬時に表示されます。
- キャッシュがない? undefined を返し、ローディングを表示します。
-
ステップ2:バックグラウンドリクエスト(revalidate)
- キャッシュの有無にかかわらず、API リクエストを発行します。
- ユーザーは既にコンテンツを見ているので、気づきません。
-
ステップ3:データの更新
- API が戻ってきたら、ひっそりとキャッシュと UI を更新します。
- データが変わっていれば、React が自動的に再レンダリングします。
- 変わっていなければ、何もしません。
実際の例を見てみましょう。株価表示ページを作っています:
function StockPrice({ symbol }) {
const { data, error } = useSWR(`/api/stock/${symbol}`, fetcher);
return (
<div>
<h2>{symbol}</h2>
<p>価格: {data ? `$${data.price}` : 'Loading...'}</p>
<span>更新時間: {data?.updatedAt}</span>
</div>
);
}
ユーザーが初めてページを開くと、data は undefined なので「Loading…」が表示されます。1秒後に API が戻り、価格が表示されます。
ユーザーが別のタブでメールをチェックし、10分後に戻ってくると——瞬時に価格(キャッシュからの)が表示され、ローディングのちらつきはありません。しかし同時に、SWR はバックグラウンドで新しいリクエストを送っています。株価が変わっていれば、ページは自動更新され、変わっていなければそのままです。
この体験は、本当にスムーズです。
自動再検証(Revalidation)の3つのタイミング:
SWR はデフォルトで以下の状況で自動的にデータを再取得します:
- コンポーネントの再マウント(revalidateOnMount):ページの更新やルートの切り替え時
- ウィンドウへのフォーカス(revalidateOnFocus):ブラウザタブに戻ってきた時
- ネットワークの回復(revalidateOnReconnect):オフラインからオンラインに戻った時
これは何を意味するか? あなたは何もする必要がありません。SWR がデータを新鮮に保ってくれます。ユーザーが30分メールを見てサイトに戻ってくれば、SWR が自動リフレッシュします。地下鉄でネットが切れても、駅について 4G が復活すれば、SWR が自動リロードします。
正直、これらがデフォルト挙動だと知った時は驚きました。以前は visibilitychange や online イベントを監視して手動でコードを書いていたのに。今は? 全部タダで付いてきます。
重要な「Key」の概念:
useSWR の第一引数が '/api/users' のような文字列であることに気づいたでしょう。これが「key」です。非常に重要です。
// 2つのコンポーネントが同じ key を使用
function Header() {
const { data } = useSWR('/api/user', fetcher);
return <div>ようこそ, {data?.name}</div>;
}
function Profile() {
const { data } = useSWR('/api/user', fetcher);
return <div>プロフィール: {data?.email}</div>;
}
key が同じなら、データは共有されます。SWR は API リクエストを1回しか送らず、両方のコンポーネントが同じデータを共有し、自動的に同期更新されます。
これが「複数コンポーネント間のデータ同期」問題を解決すると言った理由です。状態管理も Context も不要。ただ key があればいいのです。
キャッシュ戦略深掘り:データ取得をよりスマートに
SWR は自動でキャッシュし再検証しますが、実際のプロジェクトではデータの「鮮度要件」は様々です。ユーザーアバターは1週間変わらないかもしれませんが、株価は秒単位で更新が必要です。ここでキャッシュ戦略の調整が必要になります。
デフォルトは賢いが、万能ではない
SWR のデフォルト設定は実はかなりアグレッシブ(積極的)です:
- マウント時に毎回再検証
- フォーカス時に毎回再検証
- 再接続時に毎回再検証
リアルタイム性が高いデータ(チャット、オンライン状態)には最適ですが、比較的安定したデータ(記事リスト、プロフィール)には少々無駄です。ユーザーがタブを行き来するたびにリクエスト? 必要ありません。
主要な設定オプション
SWR はいろいろな設定を提供していますが、頻繁に使うのはこれらです:
const { data } = useSWR('/api/articles', fetcher, {
revalidateOnFocus: false, // フォーカス時に再取得しない
revalidateOnReconnect: false, // 再接続時に再取得しない
refreshInterval: 0, // ポーリング間隔(ms)、0はポーリングなし
dedupingInterval: 2000, // 2秒以内の同じリクエストは重複排除
});
名前が直感的なので、見ればわかりますね。
シナリオ1:リアルタイムデータ(株価、オンライン人数)
const { data } = useSWR('/api/stock/AAPL', fetcher, {
refreshInterval: 1000, // 1秒ごとにポーリング
revalidateOnFocus: true // 戻ってきたら即更新
});
時効性が高いデータです。ポーリングが最も簡単な解決策です(WebSocket の方が良いですが、それは別の話)。
シナリオ2:比較的安定したデータ(プロフィール、記事リスト)
const { data } = useSWR('/api/profile', fetcher, {
revalidateOnFocus: false, // 毎回更新しなくていい
refreshInterval: 0, // ポーリング不要
dedupingInterval: 60000, // 1分間は重複リクエストしない
});
プロフィールは頻繁に変わりません。タブ切り替えごとのリクエストは無駄です。ユーザーが自分でプロフィール編集をした時だけ手動更新(後述の mutate)すれば十分です。
シナリオ3:ほぼ静的なコンテンツ(ドキュメント、ヘルプページ)
const { data } = useSWR('/api/docs', fetcher, {
revalidateOnFocus: false,
revalidateOnReconnect: false,
revalidateOnMount: false, // マウント時すら再検証しない
revalidateIfStale: false, // データが古くても再検証しない
});
1ヶ月変わらないようなデータ。初回ロード後はほぼ永久キャッシュでOKです。ユーザーが手動リロードしない限り。
グローバル設定 vs ローカル設定
アプリ全体で戦略を統一したい場合、SWRConfig でデフォルト値を設定できます:
import { SWRConfig } from 'swr';
function App() {
return (
<SWRConfig value={{
refreshInterval: 3000,
fetcher: (url) => fetch(url).then(r => r.json()),
revalidateOnFocus: false,
}}>
<Dashboard />
</SWRConfig>
);
}
これで全子コンポーネントの useSWR がこの設定を継承します。もちろん個別に override も可能です。
リクエスト重複排除:API クォータの節約
非常に実用的な機能「自動リクエスト重複排除(Deduping)」があります。3つのコンポーネントが同時にマウントされ、すべて useSWR('/api/user') を呼んでいたとします。SWR は3回リクエストを送るのではなく、1回だけ送り、結果を共有します。
この「重複排除ウィンドウ」はデフォルトで2秒(dedupingInterval: 2000)です。つまり2秒以内の同一リクエストはマージされます。
正直、この機能には助けられました。以前、リストページでユーザーが高速スクロールするとデータロードが何度もトリガーされるバグ(私のロジックミス)がありましたが、この重複排除のおかげで、少なくとも API 制限には引っかからずに済みました(笑)。
条件付き Fetching:依存リクエスト
Aデータを取得し、その結果を使ってBデータを取得したい場合があります。SWR の小技——key に null を渡すとリクエストが一時停止します:
// 先にユーザー情報を取得
const { data: user } = useSWR('/api/user', fetcher);
// user のロードが終わったら、そのプロジェクト一覧を取得
const { data: projects } = useSWR(
user ? `/api/projects?userId=${user.id}` : null,
fetcher
);
user が undefined の間は key が null なので SWR は何もしません。user がロードされると key が有効になり、自動的にリクエストが始まります。直列依存もこれだけです。
楽観的更新:UX 向上の秘密兵器
キャッシュは「速さ」を解決しましたが、ユーザー操作への応答という課題があります。「いいね」ボタンを押した時、API の応答を待ってからハートを赤くしますか? それとも即座に赤くして、裏でリクエストを送りますか?
これが 楽観的更新(Optimistic Update) です。操作は成功すると仮定して即座に UI を更新し、失敗したらロールバックします。リスキー? いえ、ほとんどの操作は成功します(ネット切断やサバ落ち以外)。ユーザー体験の向上は劇的です。
従来方式 vs 楽観的更新
従来:
- 「いいね」クリック
- ボタンが loading 状態(または無効化)
- 500ms〜2秒待機
- API 成功、ハートが赤くなる
- ユーザー「もっさりしてるな」
楽観的更新:
- 「いいね」クリック
- ハートが即座に赤くなる(ローカル更新)
- バックグラウンドで API リクエスト
- (通常)成功、何もしない
- (稀に)失敗、ハートが灰色に戻り「失敗しました」と表示
後者の遅延は 0ms。体感速度は爆速です。
mutate 関数:キャッシュの手動制御
SWR は mutate 関数でキャッシュを手動更新できます。
import { mutate } from 'swr';
// /api/user の再検証を手動トリガー
mutate('/api/user');
しかし楽観的更新にはもっと制御が必要です。ToDo アプリの例:
import useSWR, { mutate } from 'swr';
function TodoList() {
const { data: todos } = useSWR('/api/todos', fetcher);
const addTodo = async (text) => {
const newTodo = { id: Date.now(), text, completed: false };
// 核心:楽観的更新の設定
mutate(
'/api/todos',
async (currentTodos) => {
// 1. UIに新ToDoを即座に表示(楽観的)
const optimisticData = [...currentTodos, newTodo];
// 2. バックグラウンドでリクエスト
const savedTodo = await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo)
}).then(r => r.json());
// 3. サーバーからの正式なデータで置換
return [...currentTodos, savedTodo];
},
{
optimisticData: [...todos, newTodo], // 即時表示用データ
rollbackOnError: true, // 失敗時ロールバック
revalidate: false, // 追加検証不要
}
);
};
return (
<div>
{todos?.map(todo => <div key={todo.id}>{todo.text}</div>)}
<button onClick={() => addTodo('New task')}>Add</button>
</div>
);
}
コードは長いですがロジックは明確です:
- 「Add」クリック
- 新 ToDo をリストに即表示(
optimisticData) - POST リクエスト送信
- 成功ならサーバーからの正式データ(IDやタイムスタンプ付き)で置換
- 失敗なら更新前の状態に戻す(
rollbackOnError: true)
4つの核心オプション
mutate の options オブジェクトの重要設定:
1. optimisticData:即座に表示するデータ
optimisticData: [...todos, newTodo] // 関数も可
2. populateCache:戻り値でキャッシュを更新するか
デフォルト true。API のレスポンス(完全なデータ)でキャッシュを上書きしたい場合に便利です。
3. revalidate:再検証するか
通常 false。手動で更新したので、SWR に再確認させる必要はありません。
4. rollbackOnError:失敗時にロールバックするか
超重要。true にすると、mutate 関数がエラーを投げた場合、SWR は自動的に以前のデータに戻します。
useSWRMutation:よりエレガントな書き方
SWR 2.0 で導入された useSWRMutation Hook を使うと、より宣言的に書けます:
import useSWRMutation from 'swr/mutation';
async function updateUser(url, { arg }) {
await fetch(url, {
method: 'POST',
body: JSON.stringify(arg)
});
}
function Profile() {
const { trigger, isMutating } = useSWRMutation('/api/user', updateUser);
return (
<button
onClick={() => trigger({ name: 'John' })}
disabled={isMutating}
>
Update Name
</button>
);
}
trigger で発火、isMutating で状態管理。手動で loading 管理するよりずっと楽です。
いつ楽観的更新を使うべきか?
すべての操作に適しているわけではありません。
✅ 適している:
- いいね/ブックマーク(失敗率低、可逆)
- リスト項目の追加/削除(即時フィードバック重要)
- スイッチ切り替え(通知設定など)
- 草稿保存
❌ 適していない:
- 決済処理(確認必須)
- アカウント削除(不可逆、慎重さが必要)
- 機密情報の変更(パスワード、権限)
- サーバー計算が必要なもの(結果が予測できない)
失敗率が低く、可逆で、UX優先のシーンで使いましょう。
SWR vs React Query:どっちを選ぶ?
ここまで SWR を推してきましたが、「じゃあ React Query は?」となりますよね。
両者とも優秀で、2025年現在も活発にメンテされています。選択を間違えても死にはしませんが、適したものなら開発がより快適になります。
サイズ:SWR が軽い
- SWR: 5.3KB (gzip)
- React Query (TanStack Query): 16.2KB
複雑さ:SWR がシンプル
SWR は API が極限までシンプル。useSWR だけでほぼ完結します。5分で理解できます。
React Query は強力ですが、学習曲線が急です。QueryClient, useQuery, useMutation, queryKeys… 覚えることは多いです。
機能:React Query が強力
- 公式 DevTools:React Query には強力な DevTools があり、デバッグ体験が最高です。SWR には公式はありません。
- 詳細なキャッシュ制御:cache time と stale time を別々に制御できます。SWR は cache + revalidate のみ。
- 無限スクロール:両方ありますが、React Query の方が API が成熟しています。
私の選択アドバイス
| SWR を選ぶ時 | React Query を選ぶ時 |
|---|---|
| プロジェクトがシンプル、ロジックが単純 | 複雑、キャッシュの詳細制御が必要 |
| バンドルサイズに敏感(モバイル等) | 10KB の差は気にしない |
| Next.js を使用 | 他のフレームワーク(Vite等) |
| チームの経験が浅い | チームが複雑なライブラリに慣れている |
| 即座に開発を始めたい | エコシステム全体を学びたい |
個人的には、MVP や小〜中規模プロジェクトなら SWR 一択です。シンプルさは正義です。
Next.js と SWR:最高の組み合わせ
どちらも Vercel 製なので相性は抜群です。
App Router での使用
App Router はデフォルトで Server Components なので、SWR (クライアントライブラリ) は Client Component で使う必要があります:
'use client'; // 必須
import useSWR from 'swr';
// ...
SSR + SWR:fallback data の活用
Next.js の SSR で初期データを取得し、それを SWR の初期値(fallback)として渡すパターンが強力です:
// page.tsx (Server Component)
import { SWRConfig } from 'swr';
import { getUsers } from '@/lib/db'; // DB直接取得
import UserList from './user-list';
export default async function Page() {
const fallbackData = await getUsers();
return (
<SWRConfig value={{ fallback: { '/api/users': fallbackData } }}>
<UserList />
</SWRConfig>
);
}
// user-list.tsx (Client Component)
'use client';
import useSWR from 'swr';
export default function UserList() {
// 初回レンダリングは fallback データを使用(ローディングなし!)
// その後 SWR がバックグラウンドで更新を確認
const { data } = useSWR('/api/users', fetcher);
return <ul>...</ul>;
}
これで、SEO に有利な SSR(完全な HTML)と、クライアントサイドでのリアルタイム更新(SWR)を両立できます。完璧です。
結論
まとめましょう。
SWR は React データ取得の3大苦痛(冗長なコード、キャッシュ欠如、同期問題)を解決します。「stale-while-revalidate」戦略により、速さ(キャッシュ表示)と正確さ(バックグラウンド更新)を両立します。
ベストプラクティス:
- データ特性に合わせてキャッシュ戦略を調整:リアルタイムはポーリング、静的データは長期間キャッシュ。
- 適切な場所で楽観的更新を使う:UX が劇的に向上します。
- key を活用してデータ共有:状態管理ライブラリを減らせます。
- Next.js では fallback を活用:SSR + SWR のハイブリッド構成。
SWR は銀の弾丸ではありません。悪い API 設計は救えません。しかし、バックエンドがまともなら、フロントエンドのコードを驚くほどエレガントにしてくれます。
次のプロジェクトでは SWR を試してみませんか? きっと「なんでもっと早く使わなかったんだ」と思うはずです。
SWR完全導入フロー
インストールから設定、使用、キャッシュ最適化までのステップ
⏱️ Estimated time: 1 hr
- 1
Step1: インストールと基本使用
インストール:
```bash
npm install swr
```
基本使用:
```tsx
'use client'
import useSWR from 'swr'
const fetcher = (url: string) => fetch(url).then(r => r.json())
export function UserList() {
const { data, error, isLoading } = useSWR('/api/users', fetcher)
if (error) return <div>Failed to load</div>
if (isLoading) return <div>Loading...</div>
return <div>{data.map(user => <div key={user.id}>{user.name}</div>)}</div>
}
```
ポイント:Clinet Component ('use client') で使用し、fetcher 関数を用意するだけ。 - 2
Step2: グローバル設定
SWRConfig で全コンポーネントのデフォルト挙動を設定:
```tsx
'use client'
import { SWRConfig } from 'swr'
const fetcher = (url: string) => fetch(url).then(r => r.json())
export function Providers({ children }) {
return (
<SWRConfig
value={{
fetcher,
revalidateOnFocus: true,
revalidateOnReconnect: true,
refreshInterval: 0,
}}
>
{children}
</SWRConfig>
)
}
``` - 3
Step3: Next.js SSR 統合
fallback data を使用して、SSR の初期データを SWR に渡す:
```tsx
// app/users/page.tsx (Server Component)
import { getUsers } from '@/lib/users'
export default async function UsersPage() {
const initialUsers = await getUsers()
return (
<SWRConfig value={{ fallback: { '/api/users': initialUsers } }}>
<UserList />
</SWRConfig>
)
}
```
これで初回ロード時の空白を防ぎ、SEO も確保できます。
8 min read · 公開日: 2025年12月19日 · 更新日: 2026年1月22日
関連記事
Next.js ファイルアップロード完全ガイド:S3/Qiniu Cloud 署名付き URL 直接アップロード実践
Next.js ファイルアップロード完全ガイド:S3/Qiniu Cloud 署名付き URL 直接アップロード実践
Next.js Eコマース実践:カートと Stripe 決済の完全実装ガイド
Next.js Eコマース実践:カートと Stripe 決済の完全実装ガイド
Next.js ユニットテスト実践:Jest + React Testing Library 完全設定ガイド

コメント
GitHubアカウントでログインしてコメントできます