SWR 完全ガイド:キャッシュ戦略と楽観的更新の実践テクニック
画面上で回り続けるお馴染みのローディングスピナーを見つめながら、ユーザーリストのページを何度もリロードする。タブを切り替えるたびにローディングが走り、別のページから戻るたびにもローディング。電話に出てブラウザのフォーカスが少し外れただけでも、戻ってくるとまたローディング。一番もどかしいのは、わずか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 は自動的に以前のデータに戻します。ユーザーが「いいね」を押してハートが赤くなったのに API が失敗した? SWR が自動で灰色に戻してくれます。
関数を渡して、より細かく制御することもできます:
rollbackOnError: (error) => {
// ネットワークタイムアウトはロールバックしない、その他はロールバック
return error.name !== 'AbortError';
}
実践:ToDo の削除
削除操作も楽観的更新の定番シーンです:
const deleteTodo = async (id) => {
mutate(
'/api/todos',
async (currentTodos) => {
const optimistic = currentTodos.filter(t => t.id !== id);
await fetch(`/api/todos/${id}`, { method: 'DELETE' });
return optimistic;
},
{
optimisticData: todos.filter(t => t.id !== id),
rollbackOnError: true,
}
);
};
ユーザーが削除をクリックすると、ToDo は即座に消えます。サーバーが「存在しない」と返したりリクエストが失敗したりすれば、ToDo が再表示され、エラーが出ます。
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 優先のシーンで使いましょう。決済やアカウント削除、パスワード変更のような不可逆・高リスク操作は、素直にサーバーの応答を待ちましょう。
私がハマった話を一つ。以前、コメント機能に楽観的更新を入れました。投稿が即座に表示されて UX は最高。ところがある日、ネットが遅いユーザーが「送信」を連打していたのです——反応がない(実は送れている)と思って、何度もクリック。毎回 optimistic 更新が走り、同じコメントが十数件並んでしまいました。API 側には重複防止がありましたが、フロントの UI は乱れました。isMutating で送信中はボタンを無効化して解決。楽観的更新は気持ちいいけど、境界ケースは忘れずに。
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 が強力
React Query には SWR にない機能がいくつかあります:
-
公式 DevTools:すべての query 状態、キャッシュ内容、refetch 周期を可視化。デバッグ体験は抜群。SWR には公式 DevTools がありません。
-
より細かいキャッシュ制御:cache time(メモリに保持する時間)と stale time(いつ stale になるか)を別々に設定できます。SWR は cache + revalidate 中心で、相対的にシンプルです。
-
ページネーション / 無限クエリ:React Query には
useInfiniteQueryがあり、SWR にはuseSWRInfiniteがあります。機能は近いですが、React Query の API の方が成熟している印象です。 -
mutation サポート:React Query の
useMutationはグローバル mutation state、retry 戦略、onSettledコールバックなど設計が体系的。SWR のuseSWRMutationは 2.0 で追加された比較的新しい機能で、ややシンプルです。
コミュニティとエコシステム
React Query(現 TanStack Query)のコミュニティは大きく、NPM の週間ダウンロード数も多い。困ったとき Stack Overflow で答えを見つけやすい。
SWR は Vercel 製で Next.js と同じ家。Next.js 向けの統合ガイドが公式ドキュメントにあり、Next.js なら「親玉」扱いです。
私の選択アドバイス
| SWR を選ぶ | React Query を選ぶ |
|---|---|
| プロジェクトが比較的シンプル、データ取得ロジックが複雑でない | プロジェクトが複雑、キャッシュを細かく制御したい |
| バンドルサイズに敏感(モバイル H5 など) | 10KB 程度の差は気にしない |
| Next.js を使っている | CRA、Vite など他フレームワーク |
| フロントエンド経験が浅いチーム | 複雑なライブラリに慣れたチーム |
| すぐに手を動かしたい | エコシステム全体を学ぶ時間がある |
個人的には、小規模プロジェクトや MVP 段階では SWR を第一候補にします。シンプルで速い。プロジェクトが大きくなり要件が複雑化したら React Query へ移行することもあります。ただ、大多数のプロジェクトでは SWR で十分です。
もう一点:移行コストはそれほど高くありません。cache、revalidate、mutation といった核心概念は近い。本当に React Query の高度な機能が必要になったら、切り替えても大きな痛みは少ない。選びすぎて悩まないで大丈夫です。
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)です。getStaticProps や getServerSideProps で初期データを取得し、SWR の fallback として渡せます:
// pages/profile.js
export async function getStaticProps() {
const user = await fetch('https://api.example.com/user').then(r => r.json());
return {
props: {
fallback: {
'/api/user': user // SWR の key に対応
}
},
revalidate: 60 // ISR: 60 秒ごとに再生成
}
}
export default function Profile({ fallback }) {
return (
<SWRConfig value={{ fallback }}>
<UserProfile />
</SWRConfig>
);
}
function UserProfile() {
// 初回レンダリングは fallback データを使用、loading なし
const { data } = useSWR('/api/user', fetcher);
return <div>{data.name}</div>;
}
初回アクセスからコンテンツが見え(SEO に有利)、クライアント側では SWR が引き続き revalidate します。両取りです。
プリフェッチ(Prefetching)
重要なデータは、ユーザーがクリックする前に先読みできます:
import { mutate } from 'swr';
function ArticleLink({ id }) {
const prefetch = () => {
mutate(`/api/article/${id}`, fetch(`/api/article/${id}`).then(r => r.json()));
};
return (
<Link href={`/article/${id}`} onMouseEnter={prefetch}>
Read more
</Link>
);
}
リンクにマウスを乗せた時点で記事データの取得が始まります。クリック時にはキャッシュ済みの可能性が高く、ページが一瞬で開きます。
無限スクロール:useSWRInfinite
リストページでよくある「スクロールで追加読み込み」:
import useSWRInfinite from 'swr/infinite';
function ArticleList() {
const getKey = (pageIndex, previousPageData) => {
if (previousPageData && !previousPageData.length) return null;
return `/api/articles?page=${pageIndex + 1}&limit=10`;
};
const { data, size, setSize, isLoading } = useSWRInfinite(getKey, fetcher);
const articles = data ? data.flat() : [];
const isLoadingMore = isLoading || (size > 0 && data && typeof data[size - 1] === 'undefined');
return (
<div>
{articles.map(article => (
<div key={article.id}>{article.title}</div>
))}
<button
onClick={() => setSize(size + 1)}
disabled={isLoadingMore}
>
{isLoadingMore ? 'Loading...' : 'Load More'}
</button>
</div>
);
}
setSize(size + 1) で次のページを読み込みます。各ページはキャッシュされるので、上にスクロールして戻っても再取得不要です。
パフォーマンス最適化のポイント
Next.js で SWR を使うとき、次を意識しましょう:
-
revalidate 頻度をデータに合わせる
- 静的コンテンツ(ドキュメント、ヘルプ):自動 revalidate をオフ
- ユーザー関連データ:デフォルトの focus revalidate を維持
- リアルタイムデータ:
refreshIntervalでポーリング
-
API ルートのキャッシュ
- Next.js の API ルートは
Cache-Controlヘッダーと組み合わせ可能 - SWR は HTTP キャッシュヘッダーを尊重します
- Next.js の API ルートは
-
ウォーターフォールを避ける
- 複数データに依存関係がある場合、サーバー側でリクエストをまとめる
- または Next.js の並列データ取得を活用
実例を一つ。以前、ブログの記事一覧に SWR + ISR(Incremental Static Regeneration)を使いました。初回表示は静的 HTML で秒開き。ユーザーが読んでいる間、SWR がバックグラウンドで新着記事をチェックし、あればリストを更新。体験は滑らかで、SEO も問題なし。
これが Next.js + SWR の魅力——静的サイトの性能と、動的アプリのリアルタイム性の両立です。
結論
ここまで書いてきた内容を振り返りましょう。
SWR は React データ取得の3大苦痛——コードの冗長さ、キャッシュ不足、複数コンポーネント間の同期——を解決します。核心戦略「stale-while-revalidate」は速さ(キャッシュを即表示)と正確さ(バックグラウンド更新)を両立。自動 revalidation、リクエスト重複排除、楽観的更新まで含め、データ取得シーンの 90% 程度をカバーします。
ベストプラクティスをいくつか:
- データ特性に合わせてキャッシュ戦略を調整:リアルタイムはポーリング、安定データは長めのキャッシュ
- ユーザー操作には楽観的更新を:失敗率が低く可逆な操作に限る
- key でデータ共有:共有のために状態管理を増やさない
- Next.js プロジェクトでは SWR を優先:fallback data で SSR と組み合わせる
- シンプルなら SWR、複雑なら React Query:過剰設計しない
最後に正直な話。SWR は銀の弾丸ではありません。悪い API 設計は救えませんし、合理的なアーキテクチャの代わりにもなりません。バックエンド API がまともなら、フロントエンドのコードを驚くほどすっきりさせてくれます。
次のプロジェクト、SWR を試してみませんか? きっと「もっと早く使えばよかった」と思うはずです。
SWR 完全使用フロー
インストールから設定、使用、キャッシュ最適化までの完全ステップ
⏱️ 目安時間: 1 時間
- 1
ステップ1: インストールと基本使用
インストール:
```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>
}
```
ポイント:
• 'use client' でクライアントコンポーネントとしてマーク
• fetcher 関数でデータ取得を処理
• useSWR は data、error、isLoading を返す - 2
ステップ2: グローバルオプションの設定
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>
)
}
```
よく使うオプション:
• revalidateOnFocus:ウィンドウフォーカス時に再検証
• revalidateOnReconnect:ネットワーク再接続時に再検証
• refreshInterval:定期更新(0 は更新なし)
• dedupingInterval:重複排除間隔(デフォルト 2000ms)
ポイント:ルートコンポーネントで設定し、子コンポーネント全体で共有 - 3
ステップ3: Next.js 統合(SSR)
fallback data との組み合わせ:
```tsx
// app/users/page.tsx(Server Component)
import { getUsers } from '@/lib/users'
export default async function UsersPage() {
const initialUsers = await getUsers()
return <UsersList initialUsers={initialUsers} />
}
// components/UsersList.tsx(Client Component)
'use client'
import useSWR from 'swr'
export function UsersList({ initialUsers }) {
const { data } = useSWR('/api/users', fetcher, {
fallbackData: initialUsers // SSR データを初期値に
})
return <div>{data.map(...)}</div>
}
```
メリット:
• 初回表示は SSR データで高速
• 以降の更新は SWR でスムーズ
• SSR とクライアントキャッシュの両方の利点
ポイント:initialData ではなく fallbackData を使う(fallbackData は再検証をトリガーしない) - 4
ステップ4: 楽観的更新
mutate による楽観的更新:
```tsx
import { mutate } from 'swr'
async function updateUser(id: string, name: string) {
// 楽観的更新:UI を即座に更新
mutate(`/api/users/${id}`, { ...user, name }, false)
// リクエスト送信
await fetch(`/api/users/${id}`, {
method: 'PATCH',
body: JSON.stringify({ name })
})
// 再検証:データの正確性を確保
mutate(`/api/users/${id}`)
}
```
ポイント:
• mutate の第3引数 false は再検証しない
• リクエスト成功後に再度 mutate で再検証
• ユーザー体験が向上(更新が即座に見える)
FAQ
SWR とは?なぜ必要なのか?
従来のデータ取得の課題:
• 各取得ごとに useState、useEffect、loading、error 処理が必要
• 最低 20 行のボイラープレート
• 複数コンポーネントで同じデータが必要な場合、状態のリフトアップか重複リクエスト
SWR の強み:
• 自動キャッシュ:同じ key のリクエストはキャッシュを共有
• 自動再検証:ウィンドウフォーカス、ネットワーク再接続時に自動更新
• 自動重複排除:同じリクエストは1回だけ
• 自動エラー再試行:失敗時に自動リトライ
• 1 つの Hook でデータ取得コードの 90% を簡素化
3 行で 80% の問題を解決:
```tsx
const { data, error, isLoading } = useSWR('/api/users', fetcher)
```
SWR と React Query の違いは?
• シンプルで軽量、学習コストが低い
• 大多数のプロジェクトに適している
• 機能は比較的シンプル
• Vercel 製、Next.js との統合が良い
React Query:
• 機能が豊富で強力
• 複雑なシーン向き
• 学習コストが高い
• 柔軟だが設定も複雑
選択の目安:
• シンプルなプロジェクト → SWR
• 複雑なプロジェクト → React Query
• 迷ったら → まず SWR、必要なら移行
ポイント:過剰設計しない。シンプルなら SWR、複雑なら React Query。
SWR を Next.js でどう使う?
```tsx
// app/users/page.tsx(Server Component)
export default async function UsersPage() {
const initialUsers = await getUsers()
return <UsersList initialUsers={initialUsers} />
}
// components/UsersList.tsx(Client Component)
'use client'
export function UsersList({ initialUsers }) {
const { data } = useSWR('/api/users', fetcher, {
fallbackData: initialUsers // SSR データを使用
})
return <div>{data.map(...)}</div>
}
```
メリット:
• 初回表示は SSR データで高速
• 以降の更新は SWR でスムーズ
• SSR とクライアントキャッシュの両方の利点
ポイント:
• initialData ではなく fallbackData を使う
• fallbackData は再検証をトリガーしない
• Next.js App Router に適している
SWR のキャッシュ戦略は?
フロー:
1. 初回リクエスト:loading を表示
2. 成功:データを表示してキャッシュ
3. 再リクエスト:キャッシュデータを即表示(速い)
4. バックグラウンド再検証:静かにデータを更新(正確)
5. 更新完了:新データに差し替え(シームレス)
主要特性:
• 自動キャッシュ:同じ key で自動共有
• 自動重複排除:同じリクエストは1回
• 自動再検証:フォーカス、再接続時に更新
• 自動エラー再試行:失敗時にリトライ
設定オプション:
• revalidateOnFocus:フォーカス時に再検証
• revalidateOnReconnect:再接続時に再検証
• refreshInterval:定期更新
• dedupingInterval:重複排除間隔
メリット:UX が良く、データは常に最新に近いが、loading は見えにくい。
楽観的更新はどう実装する?
```tsx
import { mutate } from 'swr'
async function updateUser(id: string, name: string) {
// 楽観的更新:UI を即座に更新
mutate(`/api/users/${id}`, { ...user, name }, false)
// リクエスト送信
await fetch(`/api/users/${id}`, {
method: 'PATCH',
body: JSON.stringify({ name })
})
// 再検証:データの正確性を確保
mutate(`/api/users/${id}`)
}
```
ポイント:
• mutate の第3引数 false は再検証しない
• 成功後に再度 mutate で再検証
• ユーザー体験が向上(即座に更新が見える)
注意:リクエストが失敗した場合、元のデータにロールバックが必要。
SWR はどんなシーンに向いている?
• 頻繁に更新されるデータ(ユーザーリスト、通知など)
• 複数コンポーネントで同じデータが必要
• 自動キャッシュと再検証が欲しい
• Next.js プロジェクト(統合が良い)
向いていないシーン:
• 単純な静的データ(キャッシュ不要)
• 1 回だけ取得すればよいデータ(再検証不要)
• 複雑なキャッシュ要件(React Query の方が向く)
選択の目安:
• 大多数のプロジェクト → SWR
• 複雑なシーン → React Query
• シンプルなデータ → 素の fetch
ポイント:SWR のために SWR を使わない。要件に合わせて選ぶ。
8分で読めます · 公開日: 2025年12月19日 · 更新日: 2026年6月8日
関連記事
セルフホスト Dev Sandbox:Docker と Go でプレビュー環境を作る
セルフホスト Dev Sandbox:Docker と Go でプレビュー環境を作る
Cloudflare Pro か Business か?3 つの軸で判断するアップグレード判断ツリー
Cloudflare Pro か Business か?3 つの軸で判断するアップグレード判断ツリー
社内ネットワーク Docker pull タイムアウトのトラブルシューティング:DNS・プロキシ・ミラー加速の完全ガイド
コメント
GitHubアカウントでログインしてコメントできます