ReactとNext.jsにおけるキャッシュ:SWRとuseMemoの実践ガイド
フロントエンド開発の現場に入りたての頃、最近になってようやく使いこなせるようになったReactの機能を共有したいと思います。それがキャッシュです。
使用するツール
Reactのエコシステムでは、データがどこに存在するか、何を最適化したいかによって、異なるキャッシュ戦略が用意されています。
- SWR — アプリ全体でキャッシュが必要な、サーバーから取得した共有データ向け
- useMemo — コンポーネントレベルで行うコストの高いクライアントサイドの計算向け
SWR:手動再フェッチによる共有データキャッシュ
SWR はVercelが開発したReact用データフェッチライブラリです。その名前はHTTPキャッシュ戦略の stale-while-revalidate に由来しており、基本的な考え方はシンプルです。データを一度フェッチし、キーでキャッシュして、アプリ全体で再利用するというものです。
キーシステム
SWRでフェッチされるデータはすべてキーで識別されます。このキーは文字列一つでも、値の配列でも構いません。配列にすることで、動的でパラメータ化されたデータに対して強力に機能します。
['shops', search, status, page, perPage]
この配列がユニークなキャッシュ識別子として機能します。いずれかの値が変わると、SWRは新しいデータをフェッチすべきだと判断します。
実践的なカスタムフックの例
以下は、ページネーション付きのショップ一覧のためにSWRを使って構築したカスタムフックです。
export function useShops({
search = '',
status = '',
page = 1,
perPage = 20,
}: UseShopsParams) {
const offset = (page - 1) * perPage
return useSWR(
['shops', search, status, page, perPage],
() =>
fetchShops({
status,
q: search,
limit: perPage,
offset,
}),
{ keepPreviousData: true },
)
}
コンポーネントでの利用もすっきりしており、error や isValidating といった便利な状態変数も取得できます。
const { data, error, isValidating } = useShops({
search,
status,
page,
perPage,
})
ページネーションとの相性が良い理由
SWRを使う最大のメリットだと感じたのは、SWRキーに page 番号を含めることで、各ページが独自のキャッシュエントリを持てる点です。これにより、リスト全体を最初から読み込む必要がなく、以下のことが実現できます。
- すでに表示したページはキャッシュされる
- 実際に訪れたページだけがフェッチされる(スキップしたページはフェッチされない)
- キャッシュはページ遷移をまたいで保持される
さらに keepPreviousData: true オプションを使うと、次のページのデータをフェッチ中でも、空白やローディング状態を表示するのではなく、前のページのデータを表示し続けることができます。これによりページネーションの操作感がよりなめらかになります。
ミューテーションの扱い:キャッシュを最新の状態に保つ
キャッシュは便利ですが、キャッシュされたデータの再フェッチは手動で管理する必要があります。ユーザーがデータを追加・更新・削除すると、キャッシュが古くなってしまいます。SWRはこれを手動で処理するための mutate() を提供しています。
ミューテーション後のシンプルな再フェッチ
startTransition(async () => {
try {
const result = await replyToReview(reviewId, content)
void mutate(['review', reviewId])
} catch (error) {
console.error('Failed to reply to review:', error)
}
})
キーを指定して mutate() を呼び出すと、SWRはそのキャッシュエントリを無効化して再フェッチします。SWRは共有されているため、そのキーを使用しているすべてのコンポーネントが自動的に更新されたデータを受け取ります。
複雑な複数パラメータキーのミューテーション
キャッシュキーが動的で、ミューテーション時に正確な値がわからない場合もあります。SWRはそのような場合のために述語ベースのmutateをサポートしています。
const key = [
'shop-cars',
shopId,
params.vehicleType,
params.minCapacity,
params.maxPrice,
params.limit,
params.offset,
]
await mutate(
(key) => Array.isArray(key) && key[0] === 'shop-cars' && key[1] === shopId,
)
これにより、その条件に一致するすべてのキャッシュエントリが無効化されます。
useMemo:コンポーネントレベルの計算キャッシュ
メモリ上の大きなリストをフィルタリング・ソート・変換したいとき、毎回のレンダリングで再計算したくない場合があります。
そのために useMemo() があります。計算結果をキャッシュし、依存関係が変わったときだけ再計算します。
例:予約リストのフィルタリング
const filteredReservations = useMemo(() => {
if (selectedCarId === 'all') return reservations
return reservations.filter((r) => r.carId === selectedCarId)
}, [reservations, selectedCarId])
ここでは、reservations または selectedCarId が実際に変わった場合にのみフィルタが再実行されます。大きなデータセットでは、UIの応答性に意味のある差をもたらすことがあります。
useMemoを使うべき場面
useMemo が特に効果的なのは以下のような場合です。
- 大きな配列のフィルタリングやソート
- 毎回のレンダリングで実行する必要のない重い計算処理
まとめ
キャッシュは高度なトピックに思えるかもしれませんが、実際のデータを大規模に扱うようになると、UIを構築する上で欠かせない考え方になります。特にSWRは、データフェッチへのアプローチを大きく変えてくれました。
※本記事の日本語訳はAIを使用して作成しました。