App Routerへの移行で「データが.更新されない」と絶望した話:Server Actionsのキャッシュ制御にハマった記録

App Routerへの移行で「データが.更新されない」と絶望した話:Server Actionsのキャッシュ制御にハマった記録

深夜2時。目の前のブラウザには、さっきまで確かに存在していたはずの「完了済みタスク」が、なぜかまだ「未完了」として表示され続けている。

新しいプロジェクトの基盤をNext.js 14のApp Routerで作り直す際、僕は確信していた。「Server Actionsを使えば、API Routeをわざわざ書く手間も省けるし、もっとシンプルに書けるはずだ」と。確かに、フォームから直接サーバーサイドの関数を呼び出す体験は魔法のようにスムーズだった。しかし、その「魔法」のせいで、画面上のデータが更新されないという、一見すると初歩的ながら、App Router特有のキャッシュ機構に阻まれた壁にぶつかることになった。

事の発端は、単純なタスクのステータス更新機能だった。DB(Prisma)の値を書き換えるServer Actionを作成し、それを`use client`で作成したボタンから呼び出す。コード自体は非常にシンプルで、エラーも出ない。DBの中身を確認すると、確かに値は書き換わっている。なのに、ブラウザをリロードしない限り、画面上の表示は古いままなのだ。

最初は「Client ComponentのState管理が間違っているのか?」と疑い、`useState`や`useEffect`を弄り回した。しかし、根本的な原因はクライアント側ではなく、Next.jsの強力すぎるキャッシュ(Data Cache)側にあった。App RouterにおけるServer Componentsは、一度レンダリングされると、その結果がサーバー側にキャッシュされることがある。Server ActionsでDBを更新しても、Next.ックのサーバー側にある「古いレンダリング結果」がそのまま使い回されてしまうのだ。

結局、解決策は「再検証(Revalidation)」を明示的に行うことだった。`revalidatePath`を使うことで、特定のパスのキャッシュを破棄し、最新のデータを取得し直すよう指示する必要があったのだ。

以下に、僕が失敗したコードと、修正後のコードをまとめる。

// ❌ 失敗したパターン:DBは更新されるが、画面が変わらない
'use server'

import { db } from '@/lib/db'

export async function updateTaskStatus(formData: FormData) {
  const taskId = formData.get('id') as string;
  const newStatus = formData.get('status') as string;

  // DBの更新は成功しているが、これだけでは画面の表示は古いまま
  await db.task.update({
    where: { id: taskId },
    data: { status: newStatus },
  });
}

// ✅ 修正後のパターン:revalidatePathでキャッシュを明示的に更新する
'use server'

import { db } from '@/lib/db'
import { revalidatePath } from 'next/cache'

export async function updateTaskStatus(formData: FormData) {
  const taskId = formData.get('id') as string;
  const newStatus = formData.get('status') as string;

  await db.task.update({
    where: { id: taskId },
    data: { status: newStatus },
  });

  // タスク一覧のページ(/tasks)のキャッシュを無効化し、再レンダリングを促す
  revalidatePath('/tasks');
}

この`revalidatePath`を追加した瞬間、ボタンを押すと同時に画面がパッと切り替わった時の感動は、今でも忘れられない。

また、開発中にTurbopackを有効にしていた際にも、少しばかりの混乱があった。Next.jsの起動速度は劇的に向上して非常に快適なのだが、時折、古いモジュールの変更が反映されないような挙動(キャッシュの影響と思われるもの)に遭遇した。App Routerの学習コストは、単なる「書き方の違い」だけでなく、この背後にある強力なキャッシュ戦略やレンダリング・メカニズムを理解することにあるのだと痛感させられた。

Pages Router時代のような「リクエストが来たら常に動的に処理する」という感覚で挑むと、今回のような「データは変わっているのに画面が変わらない」という迷宮に迷い込むことになる。Server Actionsを使う際は、常に「この更新によって、どのパスのキャッシュを破棄すべきか?」をセットで考える癖をつけなければならない。

App Routerへの移行は、単なる技術のアップデートではなく、Webアプリケーションにおけるデータの鮮度とパフォーマンスのトレードオフを再設計する作業なのだと思う。