1. ホーム
  2. Next.js

【Next.js】revalidatePath・revalidateTag の使い方|必要なときにキャッシュを再検証する

Share

Next.js の App Router は、パフォーマンスのためにページやデータのキャッシュを積極的に行います。便利な反面、投稿を追加したのに一覧ページが古いまま、といった「更新が反映されない」問題に悩まされることもあります。時間ベースの revalidate で一定時間ごとに作り直す方法もありますが、「データが変わった瞬間」に更新したい場面ではタイムラグが気になります。そこで使うのがオンデマンド再検証(on-demand revalidation)です。この記事では next/cache が提供する revalidatePathrevalidateTag を使って、必要なときだけキャッシュを明示的に無効化する方法を、Server Action の実例とあわせて解説します。

オンデマンド再検証とは何か

オンデマンド再検証とは、コードから明示的にキャッシュを無効化して、次のアクセス時に最新のデータで作り直させる仕組みです。App Router では、fetch の結果やレンダリング済みのページがキャッシュされるため、元のデータベースを更新しても、キャッシュが残っている間は古い内容が表示され続けます。

これに対する解決策は大きく2つあります。1つは時間ベースの revalidate で「60 秒ごとに作り直す」のように期限を決める方法です。もう1つが今回扱うオンデマンド再検証で、revalidatePathrevalidateTag を呼んだそのタイミングでキャッシュを捨てます。時間ベースが「一定間隔で自動的に古くなる」のに対し、オンデマンドは「データを更新したこちらの都合で、狙って無効化できる」のが違いです。投稿の作成・編集・削除のように、変更が起きた瞬間に確実に反映させたい操作と相性がよい方法です。

なお、時間ベースの revalidatefetch のキャッシュそのものについては別記事で詳しく扱っているため、ここではオンデマンドの2つの関数に絞って説明します。どちらも next/cache からインポートして使います。

revalidatePath でパス単位に無効化する

revalidatePath は、指定した URL パスに紐づくキャッシュをまとめて無効化する関数です。第1引数に無効化したいパスを渡すと、そのページのデータキャッシュとレンダリング結果が捨てられ、次のアクセスでサーバー側が作り直します。もっとも基本的な使い方は次のとおりです。

revalidatePath の基本
import { revalidatePath } from 'next/cache';

// /blog ページのキャッシュを無効化する
revalidatePath('/blog');

これで /blog のキャッシュが破棄され、次に誰かが /blog を開いたときには最新のデータでレンダリングされます。ブログ記事を1件追加したあとに一覧ページを最新化したい、といったケースでそのまま使えます。

動的ルートをまとめて無効化する

revalidatePath の第2引数には、パスの種類を表す type を渡せます。[slug] のような動的セグメントを含むページを、個別の URL ではなくルートの形でまとめて無効化したいときに使います。

type を指定した無効化
import { revalidatePath } from 'next/cache';

// /blog/[slug] に一致するすべてのページを無効化
revalidatePath('/blog/[slug]', 'page');

// (main) のようなレイアウト配下をまとめて無効化
revalidatePath('/blog', 'layout');

第2引数に 'page' を指定すると、/blog/[slug] という動的ルートに一致するページ全体が対象になります。'layout' を指定した場合は、そのレイアウトを共有する配下のページがまとめて無効化されます。特定の1ページだけを狙うなら第2引数は不要で、revalidatePath('/blog/hello-world') のように実際の URL を渡します。

revalidateTag でタグ単位に無効化する

revalidateTag は、パスではなく「タグ」を基準にキャッシュを無効化する関数です。あらかじめ fetch にタグを付けておき、そのタグを指定して呼ぶと、同じタグが付いたキャッシュがページをまたいで一斉に無効化されます。まずはタグの付け方から見てみましょう。

app/blog/page.tsx
export default async function BlogPage() {
  // fetch のキャッシュに 'posts' タグを付ける
  const res = await fetch('https://api.example.com/posts', {
    next: { tags: ['posts'] },
  });
  const posts = await res.json();

  return (
    <ul>
      {posts.map((post: { id: string; title: string }) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

ポイントは fetch の第2引数にある next: { tags: ['posts'] } です。これでこの fetch のキャッシュに posts というタグが結びつきます。タグは1つの fetch に複数付けることもでき、同じタグを複数のページの fetch に付けておけば、それらをまとめて扱えます。あとは、無効化したいタイミングで次のように呼ぶだけです。

タグを指定して無効化
import { revalidateTag } from 'next/cache';

// 'posts' タグが付いたキャッシュをすべて無効化
revalidateTag('posts');

これを呼ぶと、posts タグが付いた fetch のキャッシュが、どのページのものであっても一斉に無効化されます。「記事一覧」「サイドバーの新着」「サイトマップ」など、同じデータを参照している複数の箇所を、1回の呼び出しでまとめて最新化できるのがタグ方式の強みです。パスを1つずつ列挙する必要がありません。

Server Action で投稿を作成して一覧を最新化する

実際のアプリでは、これらの関数はデータを変更する処理とセットで呼びます。ここでは投稿を作成する Server Action を例に、データベースを更新したあとに revalidateTag で一覧を最新化する流れを見てみます。

app/actions.ts
'use server';

import { revalidateTag } from 'next/cache';
import { db } from '@/lib/db';

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;

  // 1. データベースに新しい投稿を保存する
  await db.post.create({ data: { title } });

  // 2. 'posts' タグのキャッシュを無効化して一覧を最新化する
  revalidateTag('posts');
}

流れはシンプルです。まずデータベースに投稿を保存し、そのあとに revalidateTag('posts') を呼びます。先ほど posts タグを付けておいた一覧ページの fetch キャッシュが無効化されるので、フォーム送信後に一覧を開けば、追加した投稿がすぐに表示されます。フォーム側は次のように、この Server Action を action に渡すだけです。

app/new/page.tsx
import { createPost } from '../actions';

export default function NewPostPage() {
  return (
    <form action={createPost}>
      <input name="title" type="text" />
      <button type="submit">投稿する</button>
    </form>
  );
}

特定の一覧ページだけを更新すればよいなら、revalidateTag('posts') の代わりに revalidatePath('/blog') を呼んでも同じ目的を達成できます。タグを付ける手間が不要なぶん手軽ですが、複数ページにまたがるデータの更新にはタグのほうが向いています。

revalidatePath と revalidateTag の使い分け

2つの関数はどちらもキャッシュを無効化しますが、基準にするものが異なります。revalidatePath は「この URL のページ」を単位にし、revalidateTag は「このタグが付いたデータ」を単位にします。特徴を並べると次のようになります。

関数無効化の単位向いている場面
revalidatePathURL パス(ページ・レイアウト)更新対象のページが明確なとき。タグの準備が不要で手軽
revalidateTagfetch に付けたタグ(データ)同じデータを複数ページで使い、まとめて更新したいとき

目安としては、「更新したいページが1〜2枚に限られていて場所がはっきりしている」なら revalidatePath、「同じデータがサイト内のあちこちで使われていて、どのページに出ているか数えきれない」なら revalidateTag が扱いやすいです。両方を組み合わせて、データ単位はタグ・特定ページはパス、と使い分けても構いません。

思ったようにキャッシュが更新されないとき

この2つの関数は仕組みを理解していないと空振りしやすく、「呼んでいるのに反映されない」といった相談が多いポイントです。代表的な原因を順に見ていきます。

Server Component の中では呼べない

revalidatePathrevalidateTag は、Server Action か Route Handler の中で呼ぶことが前提の関数です。ページをレンダリングする途中の Server Component から呼ぶことはできません。レンダリング中は「表示するためにキャッシュを読んでいる」最中であり、そこでキャッシュを無効化する操作は認められていないためです。データを更新する処理は Server Action('use server')や app/api/.../route.ts の Route Handler に切り出し、その中で再検証を呼ぶようにします。

タグを付け忘れると revalidateTag が効かない

revalidateTag('posts') が効くのは、どこかの fetchnext: { tags: ['posts'] } でそのタグを付けている場合だけです。タグを付けていない fetch は、いくら同じ名前でタグを無効化しても対象になりません。無効化のつもりのタグ名と、fetch に付けたタグ名が一致しているか、スペルも含めて確認してください。逆に、キャッシュしていない(cache: 'no-store' などの)fetch はそもそもキャッシュされないため、再検証の対象にならない点も押さえておきましょう。

開発サーバーでは挙動が分かりにくい

開発サーバー(next dev)では、動作確認をしやすくするためにキャッシュの扱いが本番と異なり、毎回データを取り直すように振る舞う場面があります。そのため「開発中は常に最新に見えるのに、本番でだけ古いまま」ということが起こり得ます。キャッシュや再検証の挙動を正しく確認したいときは、next build して next start で起動した本番相当の環境で試すのが確実です。

まとめ

revalidatePathrevalidateTag は、App Router のキャッシュを「データが変わった瞬間」に無効化するためのオンデマンド再検証の関数です。revalidatePath は URL パス単位で、動的ルートやレイアウトを type 指定でまとめて無効化できます。revalidateTagfetch に付けたタグ単位で、同じデータを使う複数ページを1回でまとめて最新化できます。実際には、投稿を保存する Server Action の中でデータベース更新のあとに呼ぶ、という形で組み込むのが定番です。どちらも Server Component からは呼べず Server Action や Route Handler の中で使うこと、revalidateTagfetch にタグを付けておかないと効かないこと、開発サーバーでは本番とキャッシュ挙動が異なることを押さえておけば、更新が反映されないトラブルの多くは避けられます。

参考ページ