Next.js App RouterでRequest Memorizationが効かない!React cacheで解決した話
はじめに
みなさん、Next.js App Routerで開発していて、こんな経験はありませんか?
「同じページで複数のコンポーネントから同じAPIを呼んでいるのに、なぜか全部リクエストが飛んでる...」
私は最近、まさにこの問題に遭遇しました。Next.jsの公式ドキュメントには「Request Memoization」という素敵な機能があると書いてあるのに、なぜか効いてくれない。
今回は、この問題の原因と、React cacheを使った解決方法について共有したいと思います。
そもそもRequest Memorizationって?
Next.jsのドキュメントによると:
React extends the fetch API to automatically memoize requests that have the same URL and options.
つまり、同じURLとオプションでfetchを呼んだら、自動的にキャッシュしてくれるらしいんです。便利ですね!
でも、ここに大きな落とし穴がありました...
実際に起きた問題
こんな感じのページ構成があったとします:
// app/items/page.tsx
export default function ItemsPage() {
return (
<>
<HeaderSection />
<SidebarSection />
<MainContent />
<FooterSection />
</>
);
}
それぞれのコンポーネントで、同じAPIを呼んでいました:
// 各コンポーネント内で
const data = await getItems();
理論上は、Request Memorizationによって1回のリクエストで済むはず...と思っていたのですが、サーバーログを見ると:
[API Call] /api/items - 10:00:00.100
[API Call] /api/items - 10:00:00.105
[API Call] /api/items - 10:00:00.110
[API Call] /api/items - 10:00:00.115
4回もリクエストが飛んでる・・・
なぜRequest Memorizationが効かなかったのか
原因1: オブジェクトの参照が違う
多くの開発者(私も含めて)は、こんな感じのHTTPクライアントラッパーを作りがちです:
// lib/api-client.ts
async function createRequestOptions() {
const token = await getAuthToken();
return {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
};
}
export const apiClient = {
async get(endpoint: string) {
const options = await createRequestOptions(); // ← ここが問題!
return fetch(`${API_URL}${endpoint}`, options);
},
};
何が問題かわかりますか?
createRequestOptions()
は毎回新しいオブジェクトを返しているんです。JavaScriptでは:
{} !== {} // true(異なるオブジェクト)
なので、Next.jsから見ると「これは別のリクエストだ」と判断されてしまうんですね。
原因2: Request Memorizationの制約
実は、Request Memorizationには結構厳しい条件があります:
-
fetch
APIを使うこと(axiosとかは対象外) - GETメソッドであること
- 完全に同一のURL、メソッド、ヘッダーであること
- Reactのレンダリング中であること
特に3番目の「完全に同一」というのがクセモノでした。
React cacheで解決!
そこで登場するのがReact cache
です。
import { cache } from 'react';
// Before: 普通の関数
export async function getItems() {
return apiClient.get('/items');
}
// After: cacheでラップ
export const getItems = cache(async () => {
return apiClient.get('/items');
});
いざ、実行してみると:
[API Call] /api/items - 10:00:00.100
// 2回目以降はキャッシュから返される(ログなし)
1回だけになりました〜〜!
Request Memoization vs React cache
両者の違いを整理してみましょう:
Request Memoizationの特徴
メリット:
- 設定不要で自動的に動く
- Next.jsの拡張機能と相性が良い
デメリット:
- fetchしか使えない
- オブジェクトの参照に気を使う必要がある
- デバッグが難しい
向いているケース:
// シンプルで静的なfetch
export async function getPublicData() {
return fetch('/api/public-data').then(r => r.json());
}
React cacheの特徴
メリット:
- どんなHTTPクライアントでも使える
- 明示的で分かりやすい
- デバッグしやすい
デメリット:
- 明示的にラップする必要がある
向いているケース:
// 認証が必要な複雑な処理
export const getPrivateData = cache(async (userId: string) => {
const token = await getAuthToken();
// 好きなHTTPクライアントが使える!
const response = await axios.get(`/api/users/${userId}`, {
headers: { Authorization: `Bearer ${token}` }
});
// データの加工もできる
return transformData(response.data);
});
実践的な使い分け
React cacheを使うべき場面
ほとんどの実世界のアプリケーションでは、React cacheの方が便利です:
- 認証が必要な場合
export const getAuthenticatedData = cache(async () => {
// 動的にトークンを取得
const token = await getAuthToken();
// ... APIコール
});
- 複数のAPIを組み合わせる場合
export const getDashboardData = cache(async () => {
const [users, stats, notifications] = await Promise.all([
fetchUsers(),
fetchStats(),
fetchNotifications(),
]);
return { users, stats, notifications };
});
- エラーハンドリングが必要な場合
export const getDataWithRetry = cache(async (id: string) => {
try {
return await fetchWithRetry(`/api/data/${id}`);
} catch (error) {
// カスタムエラーハンドリング
throw new AppError('データの取得に失敗しました', { id });
}
});
Request Memorizationが活きる場面
シンプルで変更の少ないケースでは、Request Memorizationも選択肢です:
// 静的なコンテンツの取得
const STATIC_OPTIONS = {
headers: { 'Content-Type': 'application/json' }
};
export async function getStaticContent() {
// optionsを外部で定義して参照を保つ
return fetch('/api/content', STATIC_OPTIONS);
}
まとめ
Request Memorizationが効かない問題に遭遇して、最初は「なんで??」と頭を抱えました。ドキュメント少ないし。私の理解が浅いだけかもしれませんが。
学んだこと:
- JavaScriptのオブジェクト比較は参照で行われる(
{} !== {}
) - Request Memorizationは便利だけど制約が多い
- React cacheの方が実用的で柔軟
結論:
実際のプロダクション環境では、React cacheを使う方が幸せになれることが多いです。特に認証が絡む場合は、迷わずReact cacheを選びましょう。
もし「Request Memorizationが効かない!」と悩んでいる方がいたら、この記事が少しでも役に立てば嬉しいです。
Discussion