🙌

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には結構厳しい条件があります:

  1. fetch APIを使うこと(axiosとかは対象外)
  2. GETメソッドであること
  3. 完全に同一のURL、メソッド、ヘッダーであること
  4. 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の方が便利です:

  1. 認証が必要な場合
export const getAuthenticatedData = cache(async () => {
  // 動的にトークンを取得
  const token = await getAuthToken();
  // ... APIコール
});
  1. 複数のAPIを組み合わせる場合
export const getDashboardData = cache(async () => {
  const [users, stats, notifications] = await Promise.all([
    fetchUsers(),
    fetchStats(),
    fetchNotifications(),
  ]);
  
  return { users, stats, notifications };
});
  1. エラーハンドリングが必要な場合
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が効かない問題に遭遇して、最初は「なんで??」と頭を抱えました。ドキュメント少ないし。私の理解が浅いだけかもしれませんが。

学んだこと:

  1. JavaScriptのオブジェクト比較は参照で行われる({} !== {}
  2. Request Memorizationは便利だけど制約が多い
  3. React cacheの方が実用的で柔軟

結論:

実際のプロダクション環境では、React cacheを使う方が幸せになれることが多いです。特に認証が絡む場合は、迷わずReact cacheを選びましょう。

もし「Request Memorizationが効かない!」と悩んでいる方がいたら、この記事が少しでも役に立てば嬉しいです。

参考資料

Discussion