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

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

画面上で回り続けるお馴染みのローディングスピナーを見つめながら、ユーザーリストのページを何度もリロードする。タブを切り替えるたびにローディングが走り、別のページから戻るたびにもローディング。電話に出てブラウザのフォーカスが少し外れただけでも、戻ってくるとまたローディング。一番もどかしいのは、わずか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 は自動的に以前のデータに戻します。ユーザーが「いいね」を押してハートが赤くなったのに 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 にない機能がいくつかあります:

  1. 公式 DevTools:すべての query 状態、キャッシュ内容、refetch 周期を可視化。デバッグ体験は抜群。SWR には公式 DevTools がありません。

  2. より細かいキャッシュ制御:cache time(メモリに保持する時間)と stale time(いつ stale になるか)を別々に設定できます。SWR は cache + revalidate 中心で、相対的にシンプルです。

  3. ページネーション / 無限クエリ:React Query には useInfiniteQuery があり、SWR には useSWRInfinite があります。機能は近いですが、React Query の API の方が成熟している印象です。

  4. 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)です。getStaticPropsgetServerSideProps で初期データを取得し、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 を使うとき、次を意識しましょう:

  1. revalidate 頻度をデータに合わせる

    • 静的コンテンツ(ドキュメント、ヘルプ):自動 revalidate をオフ
    • ユーザー関連データ:デフォルトの focus revalidate を維持
    • リアルタイムデータ:refreshInterval でポーリング
  2. API ルートのキャッシュ

    • Next.js の API ルートは Cache-Control ヘッダーと組み合わせ可能
    • SWR は HTTP キャッシュヘッダーを尊重します
  3. ウォーターフォールを避ける

    • 複数データに依存関係がある場合、サーバー側でリクエストをまとめる
    • または Next.js の並列データ取得を活用

実例を一つ。以前、ブログの記事一覧に SWR + ISR(Incremental Static Regeneration)を使いました。初回表示は静的 HTML で秒開き。ユーザーが読んでいる間、SWR がバックグラウンドで新着記事をチェックし、あればリストを更新。体験は滑らかで、SEO も問題なし。

これが Next.js + SWR の魅力——静的サイトの性能と、動的アプリのリアルタイム性の両立です。

結論

ここまで書いてきた内容を振り返りましょう。

SWR は React データ取得の3大苦痛——コードの冗長さ、キャッシュ不足、複数コンポーネント間の同期——を解決します。核心戦略「stale-while-revalidate」は速さ(キャッシュを即表示)と正確さ(バックグラウンド更新)を両立。自動 revalidation、リクエスト重複排除、楽観的更新まで含め、データ取得シーンの 90% 程度をカバーします。

ベストプラクティスをいくつか:

  1. データ特性に合わせてキャッシュ戦略を調整:リアルタイムはポーリング、安定データは長めのキャッシュ
  2. ユーザー操作には楽観的更新を:失敗率が低く可逆な操作に限る
  3. key でデータ共有:共有のために状態管理を増やさない
  4. Next.js プロジェクトでは SWR を優先:fallback data で SSR と組み合わせる
  5. シンプルなら SWR、複雑なら React Query:過剰設計しない

最後に正直な話。SWR は銀の弾丸ではありません。悪い API 設計は救えませんし、合理的なアーキテクチャの代わりにもなりません。バックエンド API がまともなら、フロントエンドのコードを驚くほどすっきりさせてくれます。

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

SWR 完全使用フロー

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

⏱️ 目安時間: 1 時間

  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

    ステップ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

    ステップ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

    ステップ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 とは?なぜ必要なのか?
SWR は Vercel 製の React データ取得ライブラリで、核心は stale-while-revalidate(古いキャッシュを表示しつつ再検証)です。

従来のデータ取得の課題:
• 各取得ごとに 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 の違いは?
SWR:
• シンプルで軽量、学習コストが低い
• 大多数のプロジェクトに適している
• 機能は比較的シンプル
• Vercel 製、Next.js との統合が良い

React Query:
• 機能が豊富で強力
• 複雑なシーン向き
• 学習コストが高い
• 柔軟だが設定も複雑

選択の目安:
• シンプルなプロジェクト → SWR
• 複雑なプロジェクト → React Query
• 迷ったら → まず SWR、必要なら移行

ポイント:過剰設計しない。シンプルなら SWR、複雑なら React Query。
SWR を Next.js でどう使う?
fallback data で SSR と組み合わせ:
```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 のキャッシュ戦略は?
核心:stale-while-revalidate(古いキャッシュを表示しつつ再検証)

フロー:
1. 初回リクエスト:loading を表示
2. 成功:データを表示してキャッシュ
3. 再リクエスト:キャッシュデータを即表示(速い)
4. バックグラウンド再検証:静かにデータを更新(正確)
5. 更新完了:新データに差し替え(シームレス)

主要特性:
• 自動キャッシュ:同じ key で自動共有
• 自動重複排除:同じリクエストは1回
• 自動再検証:フォーカス、再接続時に更新
• 自動エラー再試行:失敗時にリトライ

設定オプション:
• revalidateOnFocus:フォーカス時に再検証
• revalidateOnReconnect:再接続時に再検証
• refreshInterval:定期更新
• dedupingInterval:重複排除間隔

メリット:UX が良く、データは常に最新に近いが、loading は見えにくい。
楽観的更新はどう実装する?
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 で再検証
• ユーザー体験が向上(即座に更新が見える)

注意:リクエストが失敗した場合、元のデータにロールバックが必要。
SWR はどんなシーンに向いている?
向いているシーン:
• 頻繁に更新されるデータ(ユーザーリスト、通知など)
• 複数コンポーネントで同じデータが必要
• 自動キャッシュと再検証が欲しい
• Next.js プロジェクト(統合が良い)

向いていないシーン:
• 単純な静的データ(キャッシュ不要)
• 1 回だけ取得すればよいデータ(再検証不要)
• 複雑なキャッシュ要件(React Query の方が向く)

選択の目安:
• 大多数のプロジェクト → SWR
• 複雑なシーン → React Query
• シンプルなデータ → 素の fetch

ポイント:SWR のために SWR を使わない。要件に合わせて選ぶ。

8分で読めます · 公開日: 2025年12月19日 · 更新日: 2026年6月8日

コメント

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