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