🧪

【Next.js15】revalidateTagとrevalidatePathを実践して理解する。

に公開

はじめに

Next.jsではfetchのキャッシュが便利な反面、「特定のページだけを手動で再検証したい」「あるデータが変わったとき、それに関連する複数ページを再生成したい」
といったケースが出てきます。
そんなとき便利なのが、revalidateTagrevalidatePath です。
本記事ではこの2つの違いや使い分け、実際のコードを通じて理解を深めます。

revalidateTagとrevalidatePathについて

revalidateTagはfetch時にタグを設定し、それに基づいてキャッシュを無効化します。一方 revalidatePathは特定のルートに紐づいたページのキャッシュを削除します。
revalidateTagrevalidatePathは、next.revalidateと比較して、「オンデマンド再検証」と呼ばれ、イベント(フォーム送信など)に基づいてデータを再検証します。
両者は以下のように使い方と再検証の対象が異なります。

関数 対象 よく使う場面
revalidateTag fetch に付けたタグ(データ単位) API や DB データの変更通知に強い
revalidatePath ページのパス(URL単位) ページ単位で静的再生成したいとき

この違いを意識しながら、まずは revalidateTag から見ていきましょう。

revalidateTag

revalidateTagは、タグとイベントに基づいてキャッシュエントリを再検証するために使用されます。まず関数に以下のオプションでタグ付けします。

export async function getUserById(id: string) {
  const data = await fetch(`https://...`, {
    next: {
      tags: ['user'],
    },
  })
}

次に、ルートハンドラーまたはサーバーアクションでrevalidateTagを呼び出します。

import { revalidateTag } from 'next/cache'
 
export async function updateUser(id: string) {
  // Mutate data
  revalidateTag('user')
}

このコードを実行すると、指定したタグに紐づくfetchのキャッシュが無効化され、データが更新されます。

revalidatePath

ルートの再検証とイベント発生時に使用されます。使用するには、ルートハンドラまたはサーバーアクションで呼び出します。

import { revalidatePath } from 'next/cache'
 
export async function updateUser(id: string) {
  // Mutate data
  revalidatePath('/profile')
}

これでprofileページが再検証されます。

revalidateTagとrevalidatePath実践

それでは実践に入っていきます。
revalidateTagrevalidatePathはサーバーアクションもしくはルートハンドラーでしか使えないようなので、今回はサーバーアクションで実際に書いて実験してみたいと思います。
※補足:これらの再検証関数は use server な環境、つまりサーバーアクションまたはルートハンドラーの中でのみ使用できます。クライアントコンポーネントや通常の event handler からは使えない点に注意しましょう。

参考:https://youtu.be/-mPm2IRkacM

以下コード例

src\app\page.jsx
import { pathAction } from "./actions/revalidate";
import Posts from "./components/Posts";
import Users from "./components/Users";

export default async function Home() {
  return (
    <>
      <form action={pathAction} className="max-w-4xl mx-auto mt-8 ">
        <button className="w-full bg-red-600 hover:bg-red-700 text-white font-semibold py-3 rounded-lg transition-colors mb-4">
          全情報を更新
        </button>
      </form>
      <div>
        <div>
          <Users />
        </div>
        <div>
          <Posts />
        </div>
      </div>
    </>
  );
}

src\app\components\Users.jsx
import { usersAction } from "../actions/revalidate";

async function getUsers() {
  const endpoint = "https://687cd72a918b6422433016e9.mockapi.io/api/test/users";
  const response = await fetch(endpoint, {
    cache: "force-cache",
    next: { tags: ["users"] },
  });
  return response.json();
}

export default async function Users() {
  const users = await getUsers();

  return (
    <div className="mt-4">
      <h1 className="text-3xl font-bold text-center mb-8">Users</h1>

      <form action={usersAction} className="max-w-4xl mx-auto mb-8">
        <button className="w-full bg-green-600 hover:bg-green-700 text-white font-semibold py-3 rounded-lg transition-colors">
          ユーザー情報を更新
        </button>
      </form>

      <div className="grid gap-4 grid-cols-4 max-w-4xl mx-auto ">
        {users.map((user) => (
          <div
            key={user.id}
            className="bg-white shadow-md rounded-xl p-6 hover:shadow-lg transition-shadow"
          >
            <p className="text-xs text-gray-500 mt-2">ID:{user.id}</p>
            <h2 className="text-sm font-semibold text-gray-800">{user.name}</h2>
          </div>
        ))}
      </div>
    </div>
  );
}

src\app\components\Posts.jsx
import { postsAction } from "../actions/revalidate";

async function getPosts() {
  const endpoint = "https://687cd72a918b6422433016e9.mockapi.io/api/test/posts";
  const response = await fetch(endpoint, {
    cache: "force-cache",
    next: { tags: ["posts"] },
  });
  return response.json();
}

export default async function Posts() {
  const posts = await getPosts();

  return (
    <div className="mt-4 mb-8">
      <h1 className="text-3xl font-bold text-center mb-8">Posts</h1>

      <form action={postsAction} className="max-w-4xl mx-auto mb-8">
        <button className="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 rounded-lg transition-colors">
          投稿情報を更新
        </button>
      </form>

      <div className="grid gap-4 grid-cols-4 max-w-4xl mx-auto ">
        {posts.map((post) => (
          <div
            key={post.id}
            className="bg-white shadow-md rounded-xl p-6 hover:shadow-lg transition-shadow"
          >
            <p className="text-xs text-gray-500 mt-2">ID:{post.id}</p>
            <h2 className="text-sm font-semibold text-gray-800">
              {post.title}
            </h2>
          </div>
        ))}
      </div>
    </div>
  );
}


src\app\actions\revalidate.js
"use server";
import { revalidatePath, revalidateTag } from "next/cache";

export async function usersAction() {
  revalidateTag("users");
}

export async function postsAction() {
  revalidateTag("posts");
}

export async function pathAction() {
  revalidatePath("/");
}

ローカルサーバーを起動してトップページを開くと、下のような画面が表示されるはずです。

この画面は、ユーザー一覧と投稿一覧の2つのコンポーネントで構成されています。
それぞれのコンポーネント内で、mockAPIからデータを取得して表示しており、各コンポーネントには情報更新用のボタンが設置されています。
ボタンのactionには、usersActionpostsActionpathActionを指定しており、それぞれ以下のように対応しています。
usersActionrevalidateTag("users")
postsActionrevalidateTag("posts")
pathActionrevalidatePath("/")
また、今回はmockAPIを使用しており、ご自身で試したい方はアカウントを作ってお試しください。

現在の僕のmockAPI画面は以下の通りで、画像内の「users」や「posts」に表示されている数字(例:4)は、それぞれのデータ件数を表しています。
この数字を変更することで、簡単にデータ件数を増減させることが可能です。

手順1:mockAPIのデータ数を変更

それでは実際に一緒に手を動かしてrevalidateTagrevalidatePathを体感しましょう。
mockAPIのuserspostsの数値を8にします。

手順2:まずはユーザー情報だけを更新してみる

まずページをリロードしてみましょう。
force-cacheが効いているので情報が更新されないことがわかると思います。
次にUsersの情報のみを更新します。

するとrevalidateTag("users")のみ走るので、Usersのみが更新されデータが8つになります。
わかりづらくて恐縮ですが、この時点でPostsはまだ4つのデータしかありません。

手順3:投稿情報も更新してみる

次にPostsのほうも更新してみましょう。

同じように8つになりました。

手順4:データ数を4に戻してrevalidatePathの効果を確認

次にrevalidatePathを体感するためにmockAPIのuserspostsの数値を4に戻します。

手順5:「全情報を更新」ボタンでページ全体を再検証

「全情報を更新」を押すと、

両方ともデータが4個になります。

おわりに

実験中に躓いたのは、Next.js15のfetchのデフォルトキャッシュがforce-cacheではなくなっていた点です。オプションを指定していなかったため、通常のリロードでもデータが更新されてしまい、意図した挙動にならず苦労しました。
また、revalidateTagrevalidatePathはサーバーアクションやルートハンドラーでしか使えないため、現在関わっている案件では利用機会がなく、正直使い道があまりないのが残念でした。とはいえ、将来のために理解を深める意味で最後まで調査・執筆を続けました。
本記事が、revalidateTagrevalidatePathの理解に少しでも役立てば幸いです。

参考
https://youtu.be/-mPm2IRkacM
https://nextjs.org/docs/app/getting-started/caching-and-revalidating
https://nextjs.org/docs/app/guides/caching
https://nextjs.org/docs/app/api-reference/functions/fetch

Discussion