React / Next.js公開 約13分

Next.js キャッシュで更新が見えないとき:refreshと再検証を分ける

保存先では数が増えたのに画面が古いままのときに。Next.jsのApp Routerで取得元と表示を別々に追い、router.refresh、誤ったパス、正しいパスの再検証を公開用ビルドで比べます。

この記事の目次
  1. 再表示を頼む操作と、保存分を無効にする操作
  2. 保存先を、Next.jsとは別の小さなサーバーにする
  3. Server Actionへ、三通りの更新を置く
  4. 取得するページと、待機・失敗の表示をそろえる
  5. 公開用ビルドで、3・4・5・6を追う
  6. 同じデータを別ページでも使う場合は、対象を見直す

保存先では数が増えているのに、画面だけが古い。router.refreshを呼んでも変わらない。この場合は、保存先、Next.jsのサーバー側キャッシュ、ブラウザーに出た表示を別々に照合すると、更新する場所を絞れます。

ReactのJSXとfetch、async関数が読める人向けに、架空の展示カードの枚数を使います。検証構成はNode.js 24系、Next.js 16.4.5のApp Router、ReactとReact DOM 19.2.0です。Cache Componentsを無効にし、明示的なforce-cacheの取得を調べます。別バージョンやuse cacheの構成へ同じ結果を一般化しません。

再表示を頼む操作と、保存分を無効にする操作

Next.jsのfetchの説明では、サーバー側のcache指定はフレームワークの永続キャッシュを制御します。ブラウザーで使うHTTPキャッシュの指定とは対象が違います。今回のGETはforce-cacheなので、一度得た結果をサーバー側で再利用できます。

router.refreshの説明にあるのは、現在の画面についてサーバーへ再度要求する動作です。サーバー側の保存済みデータを無効にする命令ではありません。サーバーが同じキャッシュを使えば、戻ってくる数も同じです。

確認する場所 今回見るもの 調べたいこと
保存先 ローカル取得元のJSON 書込みでcountが増えたか
サーバー側の取得 force-cacheを付けたGET 以前の値を再利用していないか
ブラウザー 「表示:○枚」 再検証後の結果が届いたか

CSSのHTTPキャッシュの記事は、配信されたファイルと304を調べます。今回はブラウザーのキャッシュを消すことから始めず、Next.jsがデータ取得に使う保存分を調べます。上の三つの場所を一つの「キャッシュ」と呼ばないのが出発点です。

保存先を、Next.jsとは別の小さなサーバーにする

空の練習フォルダで次を実行し、appフォルダを作ります。実在するデータは使いません。取得元はメモリーに数を持ち、終了すると消える教材です。

npm init -y
npm install --save-exact next@16.4.5 react@19.2.0 react-dom@19.2.0
mkdir app

next.config.mjsを保存します。

export default { cacheComponents: false };

続いてsource.mjsを同じフォルダへ保存します。GETで読む数は初期値3、POSTのたびに1増えます。Node.jsのHTTP APIを使い、127.0.0.1だけで待ち受けます。認証を持たない模擬取得元なので、そのまま公開する構成ではありません。

import { createServer } from 'node:http';
let count = 3;
createServer((req, res) => {
  if (req.url !== '/count') return res.writeHead(404).end();
  if (req.method === 'POST') count += 1;
  else if (req.method !== 'GET') return res.writeHead(405).end();
  res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
  res.end(JSON.stringify({ count }));
}).listen(8767, '127.0.0.1', () => console.log('Source: http://127.0.0.1:8767/count'));

ここで返すno-storeは、この取得元のHTTP応答の指定です。Next.jsのサーバー側では、後述するforce-cacheを明示して比較します。取得元をブラウザーで直接見る数と、Next.jsが再利用する数が異なる状態を作るための組合せです。

Server Actionへ、三通りの更新を置く

app/actions.jsには、保存だけ、正しいパスの再検証、異なるパスの再検証を用意します。先頭のuse serverにより、これらの関数はサーバーで実行します。保存が成功した後にだけ再検証へ進む順序です。

'use server';
import { revalidatePath } from 'next/cache';
async function increment() {
  const response = await fetch('http://127.0.0.1:8767/count', { method: 'POST', cache: 'no-store' });
  if (!response.ok) throw new Error('保存できませんでした');
}
export async function saveOnly() { await increment(); }
export async function saveAndRevalidate() {
  await increment();
  revalidatePath('/');
}

export async function saveWrongPath() {
  await increment();
  revalidatePath('/other');
}

revalidatePathの資料にあるとおり、Server Functionから現在見ているパスを再検証すると、その画面も更新されます。Route Handlerから呼ぶ場合は次の訪問時に再検証する動作なので、今回のボタン操作と混同しません。

ブラウザーから画面の更新だけを頼むapp/refresh.jsxも保存します。use clientが必要なのは、このボタンがイベントとuseRouterを使うためです。pending中は押せない表示にしています。

'use client';
import { useRouter } from 'next/navigation';
import { useTransition } from 'react';
export default function Refresh() {
  const router = useRouter();
  const [pending, startTransition] = useTransition();
  return <button type="button" disabled={pending} onClick={() => startTransition(() => router.refresh())}>
    {pending ? '更新中' : '画面だけ更新'}
  </button>;
}

このボタンは保存を行いません。数が増えたかの検証に使うのはServer Actionのボタンで、Refreshは「画面を再度要求するだけで数が変わるか」の比較用です。

取得するページと、待機・失敗の表示をそろえる

app/page.jsxはServer Componentです。connectionの資料に沿って要求を待ち、ページ全体をビルド時の表示へ固定せず、データ取得のキャッシュを観察します。connectionを呼ぶことと、明示したforce-cacheを外すことは別です。

import { connection } from 'next/server';
import { saveOnly, saveAndRevalidate, saveWrongPath } from './actions';
import Refresh from './refresh';
export default async function Page() {
  await connection();
  const response = await fetch('http://127.0.0.1:8767/count', { cache: 'force-cache' });
  if (!response.ok) throw new Error('取得できませんでした');
  const { count } = await response.json();
  return <main>
    <h1>展示カードの数(模擬データ)</h1>
    <p id="count">表示:{count}枚</p>
    <form action={saveOnly}><button type="submit">1枚追加して保存だけ</button></form>
    <form action={saveAndRevalidate}><button type="submit">1枚追加して再検証</button></form>
    <form action={saveWrongPath}><button type="submit">1枚追加して別のパスを再検証</button></form>
    <Refresh />
    <p>保存先の数は、別タブでローカルの取得元を開いて照合します。</p>
  </main>;
}

共通の外枠app/layout.jsxを保存します。

export default function Layout({ children }) {
  return <html lang="ja"><body style={{ margin: 24, fontFamily: 'sans-serif', lineHeight: 1.8 }}>{children}</body></html>;
}

待機中のapp/loading.jsxと、失敗時のapp/error.jsxをそれぞれ保存します。Next.jsのエラー処理に沿い、例外で中断したときは再試行の入口を残します。保存後に表示更新だけが失敗する場合もあるため、エラーを「未保存」とは言い切りません。

export default function Loading() { return <p role="status">読み込み中です。</p>; }
'use client';
export default function ErrorPage({ reset }) {
  return <main><h1>処理を完了できませんでした</h1>
    <p>取得元の起動と数を確認してください。保存済みの場合もあります。</p>
    <button type="button" onClick={() => reset()}>表示を再試行</button></main>;
}

この練習では取得元の形式を自分で固定しています。外部APIへ移す場合のJSON検査、認証、二重送信対策は別に設計します。Reactの状態共有と違い、表示枚数をクライアントのstateへ複製して直す例ではありません。

公開用ビルドで、3・4・5・6を追う

一つ目のターミナルでnode source.mjsを起動します。二つ目で、ビルドに成功したことを確認してから本番モードを開始します。開発時のキャッシュ動作を比較へ混ぜないため、next devは使いません。

npx next build
npx next start --hostname 127.0.0.1 --port 3000

http://127.0.0.1:3000と、取得元のhttp://127.0.0.1:8767/countを別タブで開きます。次の各ボタンを一回ずつ押し、処理完了後に取得元のタブを再読み込みして照合します。表は新しい練習フォルダでの初回実行の値です。

操作 取得元のcount Next.jsの表示
最初に開く 3 3枚
1枚追加して保存だけ 4 3枚
画面だけ更新 4 3枚
1枚追加して別のパスを再検証 5 3枚
1枚追加して再検証 6 6枚

最初の失敗は、保存後にrouter.refreshだけを呼べばよいと考えることです。取得元は4でも表示は3のままです。修正では保存後に対象パスのrevalidatePathを呼びます。最後のボタンで取得元と表示が6へそろえば、保存分の無効化を含む経路を確かめられます。

二つ目の失敗は、更新するパスを/otherへ間違えることです。関数を呼んだ事実だけでは、表示中の/で使うデータの再検証を確認できません。修正は実際の画面のパスへ戻すこと。表示を更新できたかに加えて、どのパスを対象にしたかも記録します。

同じデータを別ページでも使う場合は、対象を見直す

一つのページを再検証する今回の例では、revalidatePathが対応します。同じデータを複数ページが使うなら、パス一つの更新を全利用先の更新とみなさず、タグによる無効化など、共有データに合った範囲を設計します。常に取り直したいデータならno-storeも候補ですが、取得回数と表示要件を先に決めます。

やり直すときは、取得元だけの再起動でNext.js側のキャッシュまで消えたと考えないでください。まず正しい再検証のボタンで両者をそろえ、その時点の数を基準にして増分を追います。何度も押した後に表の6へ合わせようとして、実データを削除する必要はありません。

最後の確認では、保存だけで表示が残る、別パスでも残る、正しい再検証で一致する、の三条件を記録します。取得元を停止して保存ボタンを押すとエラー案内になることも確認できます。再試行は再保存を意味しないので、復旧後は取得元の数を先に調べます。公開・運用の無料レッスンで確認記録を作る際も、表示と保存先の値を一組にして残してください。

TRY A LESSON

記事の内容を、課題で確かめる

前提になるJavaScriptの練習 →

React・TypeScriptの学習内容 →

記事についてのお問い合わせ

記載内容へのご質問や訂正のご連絡は、hello@junkbranding.comまでお願いします。

← 記事一覧に戻る