言語を切り替える
テーマを切り替える

SWR 完全ガイド:キャッシュ戦略と楽観的更新の実践テクニック

金曜日の午後3時、私は画面上で回り続けるローディングスピナーを、ユーザーリストページでN回目のリロードをしながら見つめていました。タブを切り替えるたびにローディング、他のページから戻るたびにローディング、電話に出るためにブラウザからフォーカスを外して戻ってきただけでもローディング。一番腹立たしいのは何か? そのデータは1秒前に読み込んだばかりだということです。

あなたも似たような経験があるはずです。React プロジェクトを書く時、データ取得のたびに useStateuseEffect、さらに loadingerror の処理ロジックを書かなければなりません。最低でも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日かかりました。コミットした時、画面いっぱいの setStateuseEffect を見て、自分でも目を覆いたくなりました。

これが 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. ステップ1:キャッシュを即座に返す(stale)

    • コンポーネントのマウント時、SWR はまずローカルキャッシュをチェックします。
    • キャッシュがある? 即座に返します。ページは瞬時に表示されます。
    • キャッシュがない? undefined を返し、ローディングを表示します。
  2. ステップ2:バックグラウンドリクエスト(revalidate)

    • キャッシュの有無にかかわらず、API リクエストを発行します。
    • ユーザーは既にコンテンツを見ているので、気づきません。
  3. ステップ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 はデフォルトで以下の状況で自動的にデータを再取得します:

  1. コンポーネントの再マウント(revalidateOnMount):ページの更新やルートの切り替え時
  2. ウィンドウへのフォーカス(revalidateOnFocus):ブラウザタブに戻ってきた時
  3. ネットワークの回復(revalidateOnReconnect):オフラインからオンラインに戻った時

これは何を意味するか? あなたは何もする必要がありません。SWR がデータを新鮮に保ってくれます。ユーザーが30分メールを見てサイトに戻ってくれば、SWR が自動リフレッシュします。地下鉄でネットが切れても、駅について 4G が復活すれば、SWR が自動リロードします。

正直、これらがデフォルト挙動だと知った時は驚きました。以前は visibilitychangeonline イベントを監視して手動でコードを書いていたのに。今は? 全部タダで付いてきます。

重要な「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 楽観的更新

従来:

  1. 「いいね」クリック
  2. ボタンが loading 状態(または無効化)
  3. 500ms〜2秒待機
  4. API 成功、ハートが赤くなる
  5. ユーザー「もっさりしてるな」

楽観的更新:

  1. 「いいね」クリック
  2. ハートが即座に赤くなる(ローカル更新)
  3. バックグラウンドで API リクエスト
  4. (通常)成功、何もしない
  5. (稀に)失敗、ハートが灰色に戻り「失敗しました」と表示

後者の遅延は 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>
  );
}

コードは長いですがロジックは明確です:

  1. 「Add」クリック
  2. 新 ToDo をリストに即表示(optimisticData
  3. POST リクエスト送信
  4. 成功ならサーバーからの正式データ(IDやタイムスタンプ付き)で置換
  5. 失敗なら更新前の状態に戻す(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」戦略により、速さ(キャッシュ表示)と正確さ(バックグラウンド更新)を両立します。

ベストプラクティス:

  1. データ特性に合わせてキャッシュ戦略を調整:リアルタイムはポーリング、静的データは長期間キャッシュ。
  2. 適切な場所で楽観的更新を使う:UX が劇的に向上します。
  3. key を活用してデータ共有:状態管理ライブラリを減らせます。
  4. Next.js では fallback を活用:SSR + SWR のハイブリッド構成。

SWR は銀の弾丸ではありません。悪い API 設計は救えません。しかし、バックエンドがまともなら、フロントエンドのコードを驚くほどエレガントにしてくれます。

次のプロジェクトでは SWR を試してみませんか? きっと「なんでもっと早く使わなかったんだ」と思うはずです。

SWR完全導入フロー

インストールから設定、使用、キャッシュ最適化までのステップ

⏱️ Estimated time: 1 hr

  1. 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. 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. 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日

コメント

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

関連記事