この記事の目次
保存先では数が増えているのに、画面だけが古い。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
記事の内容を、課題で確かめる
記載内容へのご質問や訂正のご連絡は、hello@junkbranding.comまでお願いします。