レビュー済み·難易度: 中級·更新: 2026-04-18

Next.js で fetch キャッシュと revalidateTag を使ったキャッシュ制御

fetch の cache オプション(force-cache / no-store)と revalidateTag を組み合わせてデータ単位でキャッシュを制御する例。revalidatePath との使い分けも示す。

nextjsapiperformance

対応バージョン

nextjs 15react 19

前提環境

Next.js App Router の Server Component と fetch の基本を理解していること

概要

Next.js の fetch に next.tags オプションを付けてデータにタグを設定し、revalidateTag で特定タグのキャッシュだけを無効化する。revalidatePath(パス単位)との違いを示しながら、複数ページにまたがるデータキャッシュの制御パターンを実装する。

インストール

# 追加インストールは不要

実装

タグ付きデータ取得

// lib/api.ts
export type Post = { id: number; title: string; body: string };

// force-cache + tags でキャッシュし、revalidateTag で無効化できる
export async function getPosts(): Promise<Post[]> {
  const res = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=5", {
    next: { tags: ["posts"] },
    // cache: "force-cache" は Next.js 15 のデフォルト(Server Component での fetch)
  });
  if (!res.ok) throw new Error("fetch failed");
  return res.json();
}

export async function getPost(id: number): Promise<Post | null> {
  const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, {
    next: { tags: ["posts", `post-${id}`] },
  });
  if (!res.ok) return null;
  return res.json();
}

// キャッシュしない(常に最新を取得)
export async function getLatestNotice(): Promise<{ message: string }> {
  const res = await fetch("https://jsonplaceholder.typicode.com/posts/1", {
    cache: "no-store",
  });
  if (!res.ok) throw new Error("fetch failed");
  const data = await res.json();
  return { message: data.title };
}

Server Actions でキャッシュ無効化

// app/posts/actions.ts
"use server";

import { revalidateTag, revalidatePath } from "next/cache";

// posts タグを持つすべてのキャッシュを無効化(複数ページをまたいで更新)
export async function invalidatePosts() {
  revalidateTag("posts");
}

// 特定記事のキャッシュだけ無効化
export async function invalidatePost(id: number) {
  revalidateTag(`post-${id}`);
}

// パス単位での無効化(そのページのすべてのキャッシュが対象)
export async function invalidatePostsPage() {
  revalidatePath("/posts");
}

一覧ページ(Server Component)

// app/posts/page.tsx
import { getPosts } from "@/lib/api";
import { invalidatePosts } from "./actions";

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

  return (
    <main className="mx-auto max-w-xl p-8">
      <div className="mb-6 flex items-center justify-between">
        <h1 className="text-2xl font-bold">記事一覧</h1>
        <form action={invalidatePosts}>
          <button
            type="submit"
            className="rounded bg-blue-600 px-3 py-1 text-sm text-white hover:bg-blue-700"
          >
            キャッシュ更新
          </button>
        </form>
      </div>

      <ul className="space-y-2">
        {posts.map((post) => (
          <li key={post.id} className="rounded border p-3 text-sm text-gray-700">
            {post.title}
          </li>
        ))}
      </ul>
    </main>
  );
}

詳細ページ(Server Component)

// app/posts/[id]/page.tsx
import { notFound } from "next/navigation";
import { getPost } from "@/lib/api";
import { invalidatePost } from "../actions";

type Props = { params: Promise<{ id: string }> };

export default async function PostPage({ params }: Props) {
  const { id } = await params;
  const post = await getPost(Number(id));
  if (!post) notFound();

  return (
    <article className="mx-auto max-w-xl p-8">
      <h1 className="mb-4 text-xl font-bold">{post.title}</h1>
      <p className="mb-6 text-gray-600">{post.body}</p>

      <form action={invalidatePost.bind(null, post.id)}>
        <button type="submit" className="text-sm text-blue-600 hover:underline">
          この記事のキャッシュを更新
        </button>
      </form>
    </article>
  );
}

ポイント

  • fetchnext: { tags: ["posts"] } を付けることで、そのレスポンスにキャッシュタグが紐付く
  • revalidateTag("posts") を呼ぶと "posts" タグを持つすべてのキャッシュが無効化される。複数ページのデータを一括更新できる
  • revalidatePath("/posts") はそのパス(ページ)のキャッシュをすべて無効化。revalidateTag はデータ単位なのに対し、revalidatePath はページ単位
  • cache: "no-store" を指定したフェッチはキャッシュされず、毎リクエスト最新データを取得する
  • 1つの fetch に複数タグ(["posts", "post-1"])を設定できる。revalidateTag("post-1") で個別更新、revalidateTag("posts") で一括更新と使い分けられる

注意点

revalidatePath はパス単位でキャッシュを無効化するが、revalidateTag はデータタグ単位で複数ページにまたがるキャッシュを制御できる。

関連サンプル