SWR徹底解説:キャッシュ戦略とリアルタイム更新


SWR徹底解説:キャッシュ戦略とリアルタイム更新

ウェブアプリケーション開発において、データのフェッチ(取得)と管理は常に重要な課題です。特に、Reactのようなクライアントサイドレンダリングを行うフレームワークでは、APIから非同期にデータを取得し、そのローディング状態、エラー、そしてキャッシュ戦略やリアルタイムな更新への対応がアプリケーションのユーザー体験とパフォーマンスに直結します。

従来のデータフェッチは、useEffectフック内でAPI呼び出しを行い、ローディング状態やエラー状態をuseStateで管理するのが一般的でした。しかし、複数のコンポーネントで同じデータをフェッチする場合の重複リクエスト、コンポーネントのマウント・アンマウントによるデータの無駄な再フェッチ、そして何よりもキャッシュの管理やリアルタイムなデータ更新への対応は、アプリケーションが複雑になるにつれて開発者に大きな負担をかけます。

このような課題に対する強力なソリューションとして登場したのが、SWRです。SWRはVercelによって開発されたReact Hooksライブラリであり、データのフェッチ、キャッシュ、そしてリアルタイムなデータ更新を効率的かつシンプルに実現します。その名称「SWR」は、HTTP RFC 5861で提唱されているキャッシュ無効化戦略である「Stale-While-Revalidate」に由来しています。

この記事では、SWRの中核となる「Stale-While-Revalidate」戦略がどのように機能するのか、そしてその上に構築されたキャッシュ戦略と、多様な方法によるリアルタイム更新の仕組みについて、詳細かつ網羅的に解説します。SWRを効果的に活用するための理論的背景と実践的なテクニックを深く理解することで、より高速で応答性の高いモダンなウェブアプリケーション開発が可能になるでしょう。

SWRの核となる考え方:Stale-While-Revalidate (SWR)

SWRライブラリの名前にもなっている「Stale-While-Revalidate」は、ウェブキャッシュ戦略の一つです。その基本的な考え方は以下の通りです。

  1. stale(古くなったデータ)を提供する: まず、ローカルにキャッシュされているデータがあれば、それが最新でなくても即座にユーザーに表示します。これにより、ユーザーはデータの表示を待つことなく、アプリケーションの操作を開始できます。これは「表示の高速化」に貢献します。
  2. while revalidate(同時に裏で再検証する): キャッシュデータを表示している間に、裏側で新しいデータをAPIからフェッチします。
  3. Update UI: 新しいデータのフェッチが成功したら、キャッシュを更新し、UIを最新のデータで書き換えます。

この戦略の最大の利点は、ユーザー体験の向上です。初回アクセス時やキャッシュがない場合はデータのフェッチ完了を待つ必要がありますが、一度データがキャッシュされれば、以降のアクセスでは瞬時に古いデータが表示され、その後に最新データに更新されるという流れになります。これは、ローディングスピナーを表示してユーザーを待たせるよりも、はるかにスムーズで快適な体験を提供します。

従来のキャッシュ戦略では、「キャッシュを使うか、最新のデータをフェッチするか」の二者択一になることが多かったのに対し、SWR戦略は「キャッシュを使いつつ、最新データも取得する」という並行処理を行うことで、表示速度とデータの鮮度を両立させます。

SWRの基本的な使い方 (useSWRフック)

SWRをReactアプリケーションで利用する際の中心となるのは、useSWRフックです。

“`javascript
import useSWR from ‘swr’;

function Profile() {
// 1. キャッシュキー
// 2. フェッチャー関数
const { data, error, isLoading } = useSWR(‘/api/user’, fetcher);

if (error) return

データの読み込みに失敗しました

;
if (isLoading) return

データを読み込んでいます…

; // SWR v2以降
// V1以前の場合は !data && !error で判定
// if (!data && !error) return

データを読み込んでいます…

;

// データが取得できたら表示
return

こんにちは、{data.name}さん!

;
}

// データを実際にフェッチする非同期関数
async function fetcher(url) {
const res = await fetch(url);
if (!res.ok) {
const error = new Error(‘データの取得中にエラーが発生しました’);
error.status = res.status;
throw error;
}
return res.json();
}
“`

useSWRフックは、主に以下の3つの引数を取ります。

  1. key (キャッシュキー): このSWRフックが管理するデータに関連付けられる一意のキーです。通常はAPIエンドポイントのURLが使用されます。SWRは、このキーを基にキャッシュデータの参照、再検証のトリガー、データの重複排除を行います。文字列だけでなく、配列やオブジェクトもキーとして利用できます。複雑なクエリパラメータを持つAPIなどでは、オブジェクトキーが非常に有効です。
  2. fetcher (フェッチャー関数): keyに対応するデータを実際に取得するための非同期関数です。fetch APIやAxiosなどのライブラリを使用してAPIを呼び出す処理を記述します。この関数はkeyを引数として受け取ります。
  3. options (オプションオブジェクト): SWRの動作をカスタマイズするためのオプションです。キャッシュの有効期限、再検証の頻度、エラーハンドリング、初期データの指定など、多岐にわたる設定が可能です。(後述のキャッシュ戦略・リアルタイム更新の節で詳しく解説します。)

useSWRフックは、以下の主要な値を返します。

  • data: 取得に成功した最新のデータです。最初はundefinedまたはinitialDataオプションで指定した値になり、フェッチ完了後に更新されます。キャッシュがある場合は、まずキャッシュの値が返され、その後新しいデータで更新されます。
  • error: データ取得中にエラーが発生した場合に、そのエラーオブジェクトが格納されます。成功時はundefinedです。
  • isLoading: (SWR v2以降) データが現在ローディング中であるかを示すブーリアン値です。最初のフェッチ中や、再検証中のローディング状態を簡単にハンドリングできます。
  • isValidating: データが現在バックグラウンドで再検証中であるかを示すブーリアン値です。キャッシュデータが表示されている間に裏で新しいデータをフェッチしているかどうかを判断できます。
  • mutate: このフックに関連付けられたキャッシュデータを手動で更新したり、再検証をトリガーしたりするための関数です。(後述のリアルタイム更新の節で詳しく解説します。)

このシンプルなフックを使うだけで、SWRは内部的にキャッシュの管理、バックグラウンドでの再検証、ローディング・エラー状態の追跡、そして複数のコンポーネントが同じキーを使用している場合のデータ共有とリクエストの重複排除(Deduplication)を自動で行ってくれます。

SWRのキャッシュ戦略の詳細

SWRの最大の強みの一つは、その洗練されたキャッシュ管理です。SWRはグローバルなキャッシュストアを持ち、useSWRフックが使用する全てのデータはこのキャッシュによって管理されます。

キャッシュストアの構造と管理

SWRのデフォルトのキャッシュストアは、JavaScriptのMapオブジェクトを内部的に使用しています。useSWRの第一引数として渡されるkeyMapのキーとなり、フェッチャー関数によって取得されたデータがMapの値として保存されます。

“`javascript
// SWR内部のキャッシュストア(概念図)
const cache = new Map();

// useSWR(‘/api/user’, fetcher) を実行すると…
// cache.set(‘/api/user’, { data: …, error: …, isValidating: … });
“`

このキャッシュストアはアプリケーション全体で共有されます。つまり、複数のコンポーネントが同じkeyを持つuseSWRフックを使用する場合、それらはすべて同じキャッシュデータを参照し、同じバックグラウンドフェッチの結果を共有します。これがデータ取得の重複排除を可能にし、パフォーマンスを向上させます。

キャッシュキーの重要性

キャッシュキーはSWRの根幹をなす要素です。

  • ユニークな識別: 各データリソースを一意に識別します。
  • データ共有: 同じキーを持つフック間でデータを共有します。
  • 再検証のトリガー: キーを指定して特定のデータを再検証できます。
  • キャッシュ操作: キーを指定してキャッシュデータを直接操作(更新・削除)できます。

キーとしては、シンプルな文字列(例: /api/users)や、クエリパラメータを含むURL文字列(例: /api/users?page=1&limit=10)が一般的です。しかし、APIエンドポイントが同じでも、リクエストボディの内容によって結果が変わる場合や、GraphQLのようにクエリ自体が可変な場合は、文字列だけではユニークなキーを表現できません。

このような場合、SWRは配列やオブジェクトをキーとして使用することをサポートしています。SWRは内部的にこれらの複雑なキーをシリアライズ(文字列化)してキャッシュのキーとして利用します。

“`javascript
// 文字列キー
useSWR(‘/api/user/123’, fetcher);

// クエリパラメータを含む文字列キー
useSWR(‘/api/posts?sortBy=createdAt&order=desc’, fetcher);

// 配列キー (例: GraphQLクエリと変数)
const query = query GetUser($id: ID!) { user(id: $id) { id name } };
const variables = { id: ‘123’ };
useSWR([query, variables], graphqlFetcher);

// オブジェクトキー (例: POSTリクエストのボディや複雑な検索条件)
const searchParams = { category: ‘electronics’, minPrice: 10000 };
useSWR({ url: ‘/api/products/search’, params: searchParams, method: ‘POST’ }, postFetcher); // フェッチャーでkeyオブジェクトを処理する必要あり
“`

配列やオブジェクトをキーにする場合、SWRはそれらを安定した文字列に変換して使用するため、キーの順番やプロパティの順番が変わっても同じキーとして認識されるよう配慮されています。この柔軟なキーの仕組みにより、あらゆる種類のAPIリクエストに対してキャッシュ戦略を適用することが可能になります。

キャッシュの有効期限と無効化

SWRのキャッシュは、デフォルトでは「メモリキャッシュ」です。つまり、ページをリロードしたり、ブラウザタブを閉じたりするとキャッシュは失われます。これは意図的な設計であり、データの整合性を保ちつつ、開発者が明示的なキャッシュ無効化や有効期限の設定に頭を悩ませる必要がないようにするためです。

しかし、SWRの「Stale-While-Revalidate」戦略においては、厳密な「有効期限(TTL: Time To Live)」はあまり意味を持ちません。なぜなら、SWRはキャッシュが「stale(古く)」になったと判断する基準が、時間経過だけでなく、さまざまなイベントに基づいているからです。そして、staleになったデータは表示しつつ、裏でrevalidateを行うのが基本的な動作です。

キャッシュを「無効化」する主なタイミングは、以下のいずれかです。

  1. コンポーネントのマウント時: useSWRフックが初めてコンポーネントで使用される際に、SWRはまずキャッシュにデータがあるかを確認し、あれば表示します。そして、revalidateOnMountオプションが有効(デフォルトtrue)であれば、バックグラウンドで再検証を開始します。
  2. ウィンドウまたはタブへの再フォーカス時 (revalidateOnFocus): ユーザーが別のタブに切り替えたり、アプリケーションがバックグラウンドになってから再度アクティブになった際に、SWRは自動的に関連するデータを再検証します(デフォルトtrue)。これにより、ユーザーがアプリケーションに戻ってきたときに、表示されているデータが最新に近い状態であることが保証されます。
  3. ネットワーク接続の回復時 (revalidateOnReconnect): オフライン状態からオンライン状態に復帰した際に、SWRは自動的に関連するデータを再検証します(デフォルトtrue)。ネットワークエラーでデータ取得に失敗していた場合などでも、接続が回復すれば自動的に最新データを再取得しようとします。
  4. 手動による再検証 (mutate): ユーザー操作や他のAPIリクエストの完了など、特定のイベントに応じて開発者が明示的にキャッシュを無効化し、再検証をトリガーできます。これについては後述の「リアルタイム更新」のセクションで詳しく解説します。

これらの自動または手動のトリガーによってキャッシュが「stale」と判断され、バックグラウンドでの「revalidate」が実行されるのがSWRの基本的なキャッシュ無効化と更新のフローです。

キャッシュの事前投入 (Pre-fetching)

SWRは、特定のデータが必要になる前にキャッシュにデータを事前に入れておく(プリフェッチ)機能を提供します。これは、ユーザーが次にアクセスする可能性のあるページやデータを事前に読み込んでおくことで、ナビゲーション時の待ち時間をほぼゼロにするために非常に有効です。

プリフェッチは、useSWRConfigフックから取得できるmutate関数またはprefetch関数(SWR v2以降)を使用して行います。

“`javascript
import useSWR, { useSWRConfig, prefetch } from ‘swr’;

function Navigation() {
const { mutate } = useSWRConfig();
// または v2以降
// const { prefetch } = useSWRConfig();

// ユーザーリストのデータを事前に読み込む関数
const preloadUsers = async () => {
const url = ‘/api/users’;
// mutate を使用したプリフェッチ: キャッシュにデータがあればそのまま、なければフェッチしてキャッシュに入れる
// await mutate(url, fetcher, { optimisticData: [], populateCache: true, revalidate: false });
// optimisticData に空配列などを指定することで、キャッシュを初期化しておける

// v2以降の prefetch を使用したプリフェッチ: mutate よりシンプル
await prefetch(url, fetcher);

};

return (

);
}
“`

マウスオーバーや画面内の特定の要素が表示されたタイミングなどでこのプリフェッチ関数を呼び出すことで、ユーザーが実際にそのデータが必要なページに遷移した際には、既にデータがキャッシュに存在するため、瞬時の表示が可能になります。これは特に、ナビゲーションが多いアプリケーションや、予測可能なユーザーフローを持つアプリケーションでパフォーマンスを劇的に改善できます。

初期データの指定 (initialData)

SWRはinitialDataオプションを提供しており、useSWRフックがデータをフェッチする前に、キャッシュに初期値を設定することができます。これは特に、サーバーサイドレンダリング(SSR)や静的サイト生成(SSG)で取得したデータをクライアントサイドのSWRキャッシュに引き継ぐ場合や、親コンポーネントから子コンポーネントへデータを渡す際に非常に便利です。

“`javascript
// 親コンポーネント (SSRなどでデータを取得済みと仮定)
function ParentComponent({ serverData }) {
return (

);
}

// 子コンポーネント
function ChildComponent({ initialUserData }) {
// initialData として親から渡されたデータを指定
// SWRはまずこの initialUserData を表示し、その後バックグラウンドで ‘/api/user’ から最新データをフェッチする
const { data, error } = useSWR(‘/api/user’, fetcher, { initialData: initialUserData });

if (error) return

エラーが発生しました

;
if (!data) return

データを読み込んでいます…

; // initialData があればここはスキップされる

return

ようこそ、{data.name}さん

;
}
“`

initialDataを指定することで、ユーザーはコンポーネントが表示された瞬間にデータを見ることができ、その後裏側でデータの再検証が行われます。これにより、クライアントサイドでのローディング時間をゼロにすることが可能になり、非常に滑らかなユーザー体験を提供できます。ただし、initialDataはキャッシュを初期化するだけであり、再検証をスキップするわけではない点に注意が必要です(revalidateOnMount: falseなどのオプションを組み合わせることで再検証を制御できます)。

SWRによるリアルタイム更新

SWRの「Stale-While-Revalidate」戦略は、キャッシュの鮮度を保つための基本的な「リアルタイム性」を提供しますが、アプリケーションによってはさらに積極的にデータを最新の状態に保ちたい場合があります。SWRは、この要求に応えるための様々な機能を提供しています。

1. 自動再検証(Revalidation)

SWRはいくつかのイベントを検知して、自動的にキャッシュデータの再検証を行います。これらはデフォルトで有効になっており、多くの一般的なユースケースでデータの鮮度を保つのに役立ちます。

  • フォーカス時の再検証 (revalidateOnFocus):

    • ユーザーがブラウザタブを切り替え、再びアプリケーションのタブにフォーカスした際に発生します。
    • 長い間アイドル状態だったアプリケーションでも、ユーザーが操作を再開した際に最新のデータが表示されるようになります。
    • SWRConfig または useSWR オプションで revalidateOnFocus: false と設定することで無効化できます。
    • 再検証の頻度を制御するための focusThrottleInterval オプションもあります(デフォルト5秒)。これは、短時間で複数回フォーカスイベントが発生しても、指定した間隔内では最初の1回だけ再検証を実行するというものです。
  • ネットワーク接続回復時の再検証 (revalidateOnReconnect):

    • デバイスがネットワーク接続を失い、その後回復した際に発生します。
    • オフライン中に表示されていた古いデータが、オンライン復帰と同時に最新データに更新されることが期待できます。
    • SWRConfig または useSWR オプションで revalidateOnReconnect: false と設定することで無効化できます。
  • マウント時の再検証 (revalidateOnMount):

    • useSWRフックが初めてコンポーネントで使用される際に発生します。キャッシュにデータがある場合でも、バックグラウンドで再検証が実行されます。
    • これにより、キャッシュデータによる高速表示と、常に最新データを取得しようとする動きが両立されます。
    • SWRConfig または useSWR オプションで revalidateOnMount: false と設定することで無効化できます。これは、初期データが完全に最新であることが保証されている場合などに使用することがあります(例: SSRで取得したデータ+initialDataで、クライアント側での初回マウント時には再検証不要な場合など)。

これらの自動再検証は、ほとんどの場合にユーザーが意識することなくデータの鮮度を保ってくれる便利な機能です。

2. ポーリング (refreshInterval)

特定のデータを一定間隔で繰り返しフェッチしたい場合は、refreshIntervalオプションを使用します。これは、株価やチャットメッセージリスト、通知などの、頻繁に更新される可能性のある情報を表示する場合に有効です。

“`javascript
import useSWR from ‘swr’;

function StockPriceDisplay({ ticker }) {
const { data, error } = useSWR(/api/stock/${ticker}, fetcher, {
// 5秒ごとにデータを再検証
refreshInterval: 5000,
// ウィンドウが非アクティブでもポーリングを続けるか (デフォルト false)
// refreshWhenHidden: true,
});

if (error) return

株価情報の取得に失敗しました

;
if (!data) return

株価情報を読み込んでいます…

;

return

{ticker}: {data.price} ({data.change})

;
}
“`

refreshIntervalにミリ秒単位の間隔を指定すると、SWRはその間隔でバックグラウンドでデータの再検証を行います。ただし、ポーリングはサーバーに定期的に負荷をかけるため、その使用は慎重に検討し、適切な間隔を設定することが重要です。頻繁すぎるポーリングは、サーバーやクライアントのリソースを不必要に消費する可能性があります。リアルタイム性が非常に求められる場合は、後述のミューテーションや、WebSocket/Server-Sent Eventsなどのプッシュ型技術との連携を検討する方が適切な場合が多いです。

refreshWhenHiddenオプションは、ブラウザのタブがアクティブでない状態でもポーリングを続けるかどうかを制御します。デフォルトではパフォーマンスのために非アクティブ時はポーリングを停止します。

3. ミューテーション (mutate)

アプリケーション内でデータが変更された場合(例: ユーザーが記事を投稿した、TODOアイテムを完了にしたなど)、SWRキャッシュはその変更を自動的に検知しません。このような場合は、手動でキャッシュを更新するか、関連するデータを再検証する必要があります。これを実現するのがmutate関数です。

mutate関数には、大きく分けて2つの使い方があります。

  1. バウンドミューテーション: useSWRフックが返すmutate関数を使用します。これは、その特定のフックに関連付けられたキャッシュデータのみを操作します。
  2. アンバウンドミューテーション: useSWRConfigフックから取得できるグローバルなmutate関数を使用します。これは、指定したキーを持つ全てのuseSWRフック(異なるコンポーネントで使用されているものも含む)に関連付けられたキャッシュデータを操作できます。アプリケーション全体で特定のキーのデータを無効化したり更新したりする際に使用します。

どちらのmutate関数も、第一引数にキャッシュキー、第二引数に新しいデータ(またはデータを変換する関数)、第三引数にオプションオブジェクトを取ります。

基本的な使用例:手動での再検証トリガー

データ変更操作(POST, PUT, DELETEなど)を行った後に、関連するSWRキャッシュを最新の状態に保つために再検証をトリガーする最も一般的な方法です。

“`javascript
import useSWR, { useSWRConfig } from ‘swr’;

function PostList() {
const { data: posts, error } = useSWR(‘/api/posts’, fetcher);
const { mutate } = useSWRConfig(); // アンバウンドミューテーションを取得

const addPost = async (newPostData) => {
try {
// 1. APIに新しい投稿を作成するリクエストを送信
await fetch(‘/api/posts’, {
method: ‘POST’,
headers: { ‘Content-Type’: ‘application/json’ },
body: JSON.stringify(newPostData),
});

  // 2. POSTリクエストが成功したら、'/api/posts' のキャッシュを無効化し、再検証をトリガー
  //    これにより、画面上の投稿リストが最新の状態に更新される
  mutate('/api/posts');

} catch (err) {
  console.error('投稿の追加に失敗しました', err);
  // エラーハンドリング
}

};

// … 投稿リストの表示とaddPostの呼び出し
}
“`

この例では、新しい投稿を追加するAPI呼び出しが成功した後、useSWRConfigから取得したグローバルなmutate('/api/posts')を呼び出しています。これにより、アプリケーション内のどこかで/api/postsをキーとして使用している全てのuseSWRフックがキャッシュをstaleと判断し、バックグラウンドで再検証(再度/api/postsをフェッチ)を実行します。フェッチが完了次第、新しいデータでUIが更新されます。

mutate(key)のデフォルトの動作は、指定したキーのキャッシュを無効化し、関連する全てのフックの再検証をトリガーすることです。

キャッシュの直接更新

API呼び出しの結果や、API呼び出しを伴わないクライアントサイドでの変更に基づいて、キャッシュデータを直接更新することも可能です。これは特に、APIからのレスポンスに最新データが含まれている場合や、ユーザーがリスト内のアイテムを削除した際に、APIレスポンスを待たずにUIを即座に更新したい場合に有効です。

mutate(key, newData, options)の第二引数に新しいデータを渡すことで、キャッシュを直接更新できます。オプションでrevalidate: falseを指定すると、更新後に自動的な再検証を行わずにキャッシュだけを書き換えることも可能です。

“`javascript
import useSWR from ‘swr’;

function TodoItem({ todo }) {
const { data, mutate } = useSWR(/api/todos/${todo.id}, fetcher); // バウンドミューテーションを取得

const toggleComplete = async () => {
const updatedTodo = { …data, completed: !data.completed };

// 1. キャッシュを新しいデータで即座に更新 (UIがすぐに変更される)
//    revalidate: false とすることで、この時点では裏での再フェッチは行わない
await mutate(updatedTodo, { revalidate: false });

try {
  // 2. APIに更新リクエストを送信
  await fetch(`/api/todos/${todo.id}`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ completed: updatedTodo.completed }),
  });

  // 3. APIが成功したら、必要であれば再検証をトリガー(ここでは明示的にmutateを再度呼び出す必要はないが、
  //    もしAPIレスポンスがさらに別のデータを含んでいる場合は、そのデータをmutateの第二引数に渡してキャッシュを更新することもできる)
  //    あるいは、オプションで revalidate: true としておけばAPI成功後に自動で再検証される

} catch (err) {
  console.error('TODOの更新に失敗しました', err);
  // エラーが発生した場合は、必要に応じてキャッシュを元の状態に戻すなどの処理を行う
  // mutate(data, { revalidate: false }); // ロールバック
}

};

if (!data) return null; // データがない場合は何も表示しない

return (

{data.text}

);
}
“`

この例では、チェックボックスをクリックした際に、まずmutate(updatedTodo, { revalidate: false })を呼び出して、SWRキャッシュを新しい状態(完了状態を反転)で即座に更新しています。これにより、ユーザーはチェックボックスをクリックした瞬間にUIの変更を確認でき、非常にレスポンシブな体験が得られます。その後、API呼び出しをバックグラウンドで行います。API呼び出しが失敗した場合のロールバック処理も検討する必要があります。

4. オプティミスティックアップデート (Optimistic Updates)

ミューテーションの高度なテクニックとして、オプティミスティックアップデートがあります。これは、前述のキャッシュ直接更新をさらに発展させたもので、APIリクエストの結果を待たずに、成功すると仮定してUIを即座に更新する手法です。これにより、ユーザーはネットワークの遅延を感じることなく、操作に対する即時フィードバックを得られます。API呼び出しが失敗した場合は、UIを元の状態に戻します(ロールバック)。

SWRはmutate関数にoptimisticDataオプションとrollbackOnErrorオプションを提供することで、オプティミスティックアップデートを強力にサポートしています。

“`javascript
import useSWR from ‘swr’;

function LikeButton({ postId }) {
// 投稿データのSWRフック。いいね数を含むと仮定。
const { data, error, mutate } = useSWR(/api/posts/${postId}, fetcher);

const toggleLike = async () => {
if (!data) return;

// 現在のいいね数を取得
const currentLikes = data.likes;
// オプティミスティックに更新される新しいいいね数(増減)
const newLikes = data.likedByUser ? currentLikes - 1 : currentLikes + 1;

// オプティミスティックデータとして新しい状態を定義
const optimisticData = {
  ...data,
  likes: newLikes,
  likedByUser: !data.likedByUser,
};

// キャッシュをオプティミスティックデータで更新し、同時に再検証もトリガー
// ロールバックが有効なので、APIエラー時は元のデータに戻る
const options = {
  optimisticData: optimisticData, // 成功を仮定してキャッシュを即時更新
  rollbackOnError: true,         // APIエラーが発生した場合、optimisticData適用前の状態にロールバック
  populateCache: true,           // API成功時、レスポンスでキャッシュを更新
  revalidate: true               // API成功後、バックグラウンドで再検証をトリガー
};

// 第一引数はキー(バウンドmutateの場合は省略可、useSWRConfig().mutateの場合は必須)
// 第二引数は非同期関数。これがAPI呼び出しを担当し、その結果がキャッシュ更新に使われる(populateCache: trueの場合)
// 注意: mutate の第二引数に関数を渡す場合、その関数は現在のキャッシュデータを引数として受け取ることができる
try {
  await mutate(async (cachedData) => {
    // cachedData はオプティミスティックデータが適用された後のデータ(オプションで設定可能)
    // あるいは populateCache: false の場合は元のキャッシュデータ

    // APIを呼び出し、新しいいいね数を取得または操作
    const response = await fetch(`/api/posts/${postId}/like`, {
      method: data.likedByUser ? 'DELETE' : 'POST',
    });

    if (!response.ok) {
      throw new Error('いいね操作に失敗しました');
    }

    // APIからの最新データを取得(例: 更新されたいいね数など)
    const latestData = await response.json();

    // populateCache: true なので、この最新データがキャッシュに反映される
    return { ...cachedData, ...latestData }; // キャッシュデータをマージして返す

  }, options); // オプションを渡す

} catch (err) {
  console.error('いいね操作中にエラーが発生しました', err);
  // rollbackOnError: true なので、ここでは明示的なロールバックは不要だが、追加のエラーハンドリングは可能
}

};

if (!data) return null;

return (

);
}
“`

この例では、toggleLikeが呼ばれた瞬間に、API呼び出しの結果を待たずにoptimisticDataで指定した状態(いいね数の増減、ボタンテキストの変更)にUIが更新されます。その後、mutateの第二引数に渡した非同期関数(API呼び出しを行う部分)が実行されます。API呼び出しが成功すれば、populateCache: trueによってAPIからの最新データがキャッシュに反映され、revalidate: trueによってバックグラウンドで再検証がトリガーされます。API呼び出しが失敗した場合、rollbackOnError: trueによってキャッシュはオプティミスティックデータ適用前の元の状態に戻ります。

オプティミスティックアップデートはユーザー体験を大幅に向上させますが、実装はAPI呼び出しの成功・失敗時の状態管理が複雑になるため、注意が必要です。SWRはこの複雑さをoptimisticDatarollbackOnErrorオプションで効果的に軽減しています。

5. WebSocketやServer-Sent Events (SSE) との連携

SWRはデータフェッチングライブラリであり、WebSocketやSSEのようなプッシュ型通信技術を直接提供するものではありません。しかし、これらの技術とSWRを連携させることで、サーバーからのリアルタイムな更新イベントを受けて、SWRキャッシュを更新したり再検証したりすることが可能です。

“`javascript
import useSWR, { useSWRConfig } from ‘swr’;
import { useEffect } from ‘react’;

function RealtimeComponent() {
const { data, error } = useSWR(‘/api/items’, fetcher);
const { mutate } = useSWRConfig(); // グローバルなmutateを取得

useEffect(() => {
// WebSocket接続を確立
const ws = new WebSocket(‘ws://localhost:3000/items-updates’);

ws.onmessage = (event) => {
  const updateEvent = JSON.parse(event.data);

  // 例: サーバーからアイテムが追加されたイベントを受け取った場合
  if (updateEvent.type === 'ITEM_ADDED') {
    const newItem = updateEvent.payload;

    // '/api/items' のキャッシュを直接更新(既存リストに新しいアイテムを追加)
    // 新しいデータへの変換関数を渡す
    mutate('/api/items', (currentItems) => {
      // currentItems が undefined の可能性もあるので注意
      return currentItems ? [...currentItems, newItem] : [newItem];
    }, {
      revalidate: false // この更新自体では再検証しない
    });

    // あるいは、単に再検証をトリガーしたい場合
    // mutate('/api/items');
  }

  // 例: 特定のアイテムが更新されたイベントを受け取った場合
  if (updateEvent.type === 'ITEM_UPDATED') {
    const updatedItem = updateEvent.payload;
    const itemKey = `/api/items/${updatedItem.id}`;

    // 特定アイテムのキャッシュを直接更新
    mutate(itemKey, updatedItem, { revalidate: false });

    // アイテムリスト全体にも変更が影響する場合、リストのキャッシュも再検証
    // mutate('/api/items');
  }

  // ...他のイベントタイプに応じてmutateを呼び出す...
};

ws.onerror = (err) => {
  console.error('WebSocket Error:', err);
};

// コンポーネントのアンマウント時にWebSocket接続を閉じる
return () => {
  ws.close();
};

}, [mutate]); // mutate 関数は useEffect の依存配列に含めるべきか検討 (SWRConfig provider 内なら安定)

if (error) return

エラーが発生しました

;
if (!data) return

データを読み込んでいます…

;

// アイテムリストを表示
return (

    {data.map(item => (

  • {item.name}
  • ))}

);
}
“`

このパターンでは、useEffect内でWebSocket接続を確立し、サーバーからデータ更新のイベントを受け取ります。イベントの内容に応じて、useSWRConfigから取得したグローバルなmutate関数を使って、関連するSWRキャッシュを直接更新したり(例: リストに新しいアイテムを追加)、再検証をトリガーしたりします。これにより、サーバーからのプッシュ通知に基づいて、クライアントのデータ表示をリアルタイムに更新できます。

SWRのその他の便利な機能

SWRはキャッシュとリアルタイム更新の他にも、モダンなデータフェッチングに必要な多くの機能を提供しています。

  • ローディング状態とエラーハンドリング: isLoading, isValidating, error といった戻り値により、データのローディング中やエラー発生時のUI状態を簡単に管理できます。onErrorRetry オプションでエラー時のリトライ戦略をカスタマイズすることも可能です。
  • 依存関係のあるクエリ: あるデータの取得が別のデータの取得に依存する場合(例: ユーザーIDを取得してから、そのユーザーの詳細データを取得)、キーに条件分岐を含めることで簡単に実現できます。useSWR(userId ?/api/users/${userId}: null, fetcher) のように、キーが nullundefined の場合はSWRはフェッチを実行しません。
  • ページネーションと無限ローディング: useSWRInfinite フックを使用することで、ページネーションや「もっと見る」ボタン、無限スクロールなどのUIパターンにおけるデータ取得とキャッシュ管理を効率的に行うことができます。
  • カスタムフェッチャー: デフォルトの fetch APIだけでなく、AxiosやGraphQLクライアントなど、任意のデータ取得ライブラリや方法をフェッチャー関数として使用できます。
  • グローバル設定 (SWRConfig): SWRConfig プロバイダーを使用して、アプリケーション全体または特定の部分でSWRのデフォルト設定(フェッチャー関数、オプションなど)を上書きできます。これは、共通のフェッチャー関数を設定したり、特定のエリアだけ再検証の挙動を変えたい場合に便利です。

SWR vs React Query (TanStack Query)

データフェッチングとキャッシュ管理のライブラリとして、SWRと並んでよく比較されるのがReact Query(現在はTanStack Queryの一部)です。どちらも「Stale-While-Revalidate」の考え方を採用しており、多くの機能が共通していますが、いくつか違いがあります。

  • 思想とAPI: SWRは「シンプルさと最小限のAPI」を重視しています。useSWRという一つの主要なフックを中心に機能が構築されており、学習コストが比較的低いのが特徴です。React Queryはより多くの高度な機能と細やかな設定オプションを提供しており、APIも複数のフック(useQuery, useMutation, useInfiniteQueryなど)に分かれています。
  • 機能セット: React QueryはSWRよりも多くの組み込み機能を持っています。例えば、クエリのキャッシュ時間の制御(GC時間)、データ変換、selectオプション、Devtoolsなどが標準で提供されています。SWRはコア機能に絞っており、必要に応じてコミュニティの拡張機能を利用することが多いです。
  • キャッシュ戦略: どちらもSWR戦略を核としますが、キャッシュの管理方法やデフォルトの有効期限の扱いなどに微妙な違いがあります。React Queryはより明示的に「staleTime」や「cacheTime」といった概念でキャッシュのライフサイクルを制御できるのに対し、SWRはよりイベント駆動でシンプルです。
  • エコシステム: どちらも活発なコミュニティとエコシステムを持っていますが、React Queryの方が機能セットが豊富な分、複雑な要件に対応しやすい傾向があります。

どちらのライブラリを選択するかは、プロジェクトの要件や開発チームの好みによります。シンプルなデータフェッチングと効率的なキャッシュ戦略を素早く導入したい場合はSWRが強力な候補になります。より高度なキャッシュ制御、豊富な機能、Devtoolsなどを重視する場合はReact Queryが良い選択肢となるでしょう。しかし、どちらを選んでも、従来のuseEffectによる手動管理に比べて開発効率とアプリケーションパフォーマンスは大幅に向上するはずです。

SWRを効果的に利用するためのベストプラクティス

  • 適切なキャッシュキーを選ぶ: キャッシュキーはデータの識別子です。同じデータを表すキーは常に同じになるように設計しましょう。複雑なキーには配列やオブジェクトを使用することを恐れないでください。
  • SWRConfig を活用する: アプリケーション全体で共通のフェッチャー関数やデフォルトオプションを設定する際には、SWRConfig プロバイダーを利用しましょう。これにより、コードの重複を減らし、設定を一元管理できます。
  • ミューテーション (mutate) を積極的に使う: データの変更(作成、更新、削除)があった際には、必ず関連するSWRキャッシュをmutateを使って更新または無効化し、再検証をトリガーしましょう。これにより、データの整合性が保たれ、UIが最新の状態に迅速に反映されます。オプティミスティックアップデートはユーザー体験向上の強力な手段です。
  • ポーリングの頻度に注意する: refreshInterval によるポーリングは便利ですが、サーバー負荷やクライアントのリソース消費に直結します。必要なデータに対してのみ使用し、適切な間隔を設定してください。よりリアルタイム性が求められる場合は、WebSocketなどプッシュ型技術との連携を検討しましょう。
  • エラーハンドリングを適切に行う: SWRは error 戻り値を提供しますが、エラー発生時のUI表示やリトライ戦略(onErrorRetryオプション)などを適切に設定することで、より堅牢なアプリケーションになります。
  • 依存クエリには条件付きフェッチングを使う: あるデータの取得が別のデータに依存する場合、キーが準備できるまでフェッチしないように条件分岐 (key ? key : null) を使いましょう。
  • Devtoolsを利用する (サードパーティ): SWR公式のDevtoolsはまだ成熟段階ではありませんが、コミュニティによって開発されているものもあります。キャッシュの状態や再検証のタイミングなどを視覚的に確認できるDevtoolsは、デバッグや理解に非常に役立ちます。

SWRの考慮事項と限界

SWRは素晴らしいライブラリですが、万能ではありません。いくつかの考慮事項と限界を理解しておくことが重要です。

  • グローバルステート管理ではない: SWRは主にリモートデータのフェッチとキャッシュに特化しています。アプリケーション全体のUI状態や、コンポーネント間で共有されるがAPIに依存しないような複雑なローカル状態の管理には向いていません。このような場合は、Context APIやRedux、Zustandなどの専用のステート管理ライブラリと組み合わせて使用する必要があります。
  • メモリキャッシュ: デフォルトのキャッシュはメモリ上に存在するため、ページリロードやブラウザタブの閉じによって失われます。永続的なキャッシュが必要な場合は、IndexedDBやLocalStorageなどを使用したカスタムキャッシュアダプターを実装するか、別のライブラリを検討する必要があります。
  • 大規模アプリケーションでの管理: シンプルなAPIは小〜中規模アプリケーションで開発効率を上げますが、大規模で複雑なアプリケーションでは、キャッシュキーの管理やミューテーションの連携が煩雑になる可能性もゼロではありません。規約を設けたり、機能ごとに分割したりするなどの工夫が必要になる場合があります。React Queryの方が、機能が豊富な分、大規模なデータ管理に適していると感じる開発者もいるかもしれません。
  • サーバーレンダリング(SSR)との連携: Next.jsのようなフレームワークではSWRとSSRを連携させるための機能(getServerSidePropsなどで取得したデータをinitialDataとして渡すなど)が提供されていますが、他のSSR環境で完全に連携させるには追加の設定や工夫が必要になる場合があります。

これらの点を理解した上で、SWRがあなたのプロジェクトに適しているかを判断することが重要です。多くのモダンなWebアプリケーションにおいて、SWRはそのシンプルさと効率的なデータ管理戦略により、非常に有効な選択肢となります。

まとめ

SWRは、”Stale-While-Revalidate” という強力なキャッシュ戦略に基づいた、React向けのデータフェッチングライブラリです。useSWRフックを核として、データの取得、ローディング・エラー状態の管理、そして特にキャッシュの自動管理と多様なリアルタイム更新手法を提供します。

この記事では、以下の主要な点について詳しく解説しました。

  • SWRの基本概念: Stale-While-Revalidate戦略による表示の高速化とデータの鮮度維持。
  • useSWRフック: キー、フェッチャー、オプションによる基本的なデータ取得。戻り値 (data, error, isLoading, mutate) の活用。
  • キャッシュ戦略: グローバルなメモリキャッシュ、キャッシュキーの重要性(文字列、配列、オブジェクト)、自動・手動によるキャッシュ無効化(フォーカス、再接続、マウント、mutate)。キャッシュの事前投入 (prefetch, mutate) と初期データ (initialData) の利用。
  • リアルタイム更新: 自動再検証(フォーカス、再接続)、ポーリング (refreshInterval)、手動ミューテーション (mutateによる再検証トリガーやキャッシュ直接更新)、オプティミスティックアップデート (optimisticData, rollbackOnError)、そしてWebSocket/SSEとの連携方法。

SWRを活用することで、開発者はデータフェッチやキャッシュ管理の複雑さから解放され、アプリケーションのコアロジックやユーザー体験の向上に集中できます。自動的なバックグラウンド再検証やキャッシュの重複排除は、アプリケーションのパフォーマンスを大幅に向上させます。また、mutate関数を使った柔軟なキャッシュ操作とリアルタイム更新機能は、リッチで応答性の高いユーザーインターフェースを実現するための強力な武器となります。

もしあなたがReactでデータフェッチに課題を感じているなら、ぜひSWRを試してみてください。そのシンプルさと効果によって、データ管理のアプローチが変わるはずです。キャッシュとリアルタイム更新の仕組みを深く理解し、SWRが提供する機能を最大限に引き出すことで、より優れたウェブアプリケーションを開発できるようになるでしょう。


コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール