Nextjs:revalidate
2026年8月3日const res = await fetch(url, {
next: { revalidate: 500 },
});仕組み
revalidateに指定した秒数間はキャッシュされたデータが返る。Next.jsの
revalidateによるキャッシュは「Vercel の Data Cache」に保存される。
秒数経過後のリクエスト時にバックグラウンドでデータの再生成が行われる。= リクエストという「データ更新のためのトリガー」となる初回リクエストは古いデータが表示されるので犠牲になると言える。
つまりリクエストが少ないサイトだと
revalidateの秒数経過後にリクエストが全くないと、アクセス時にかなり古いデータが表示されるような事があり得る。リロードをすればバックグラウンドで再生成された最新のデータが返るが、User側はその事を知らない。以上からリクエストが少なく、かつ外部APIを気軽に
cache:no-storeにできない場合は何らかの対策が必要になる。
HTML or データ
revalidateを書く場所によって意味が変わる
page.tsxの上部:HTML全体の更新(再生成)
fetch()内:データの更新(再生成)
useEffect:
// X
useEffect(() => {
const fetchFn = async () => {
try {
const res = await fetch(
url,
{ next: { revalidate: 500 } }
);useEffect内のfetchにrevalidateを書いても、サーバー専用の機能なので意味がない。
revalidatePath
revalidatePath("/") をCRUD関数に書くとformなどでデータを追加した際にリロードなしで追加したデータが表示される。revalidatePath()は指定したパスのサーバーコンポーネントを再実行することによりJSXの再生成を行う為。
export const createPost = async (formData: FormData) => {
const title = formData.get("title")?.toString() || "";
const content = formData.get("content")?.toString() || "";
await prisma.post.create({
data: {
title,
content,
},
});
revalidatePath("/");
};revalidatePath()はルート全体の再生成なので、cacheTag / updateTagを使う方が良い。
*注意:
revalidatePath()を実行しても"use cache"を付けたデータは再実行されない。
最新のデータを表示
課題:
revalidateの仕組み上、revalidate切れ後の初回リクエストはキャッシュされた古いデータが表示される。APIルート自体へのfetch回数は気にしないが、その中で行っている「外部APIへのfetch回数」をなるべく減らしたい。
リロードアイコン等を用意してUserにリロード・コンポーネントの再マウントをさせるのはUXが悪いので避けたい。
対策:
A - APIルート側で
revalidateを使わず、クライアント側で状態として保持することで外部APIへの再fetchをしないようにする。デメリット:各User単位でのクライアントキャッシュなので、リクエストが多いのであればサーバー側へのfetch回数(外部APIへのfetch)は減らない。 -> そのケースは
revalidateを使えば充分ではある。
B - APIルート側で
revalidateを使い、本来リロードしないと反映されない部分を別の手段で実現する。デメリット:外部APIへのfetchは減るがfetch回数自体は増える/最新データの保証はない/実装が複雑になる
結論:
今回のようなケースではAをベースにする。
リクエストが増えたら
revalidateに切り替えれば良い。リクエストが多くrevalidateを使わない場合はupstash-redis等のKVにデータを保存し、そこから返す。
挙動を調べたroute.ts:
import { NextResponse } from "next/server";
export async function GET() {
const res = await fetch(
"https://time.now/developer/api/timezone/Asia/Tokyo",
// {
// next: {
// revalidate: 60,
// },
// },
);
const data = await res.json();
return NextResponse.json({ time: data.datetime });
}ライブラリ
zustand:
クライアントで生成されてクライアントで保持するデータの状態管理に適している。
tanstack-react-query:
サーバーのデータをクライアントで保持するのに適している。fetch関連の細かい設定が可能な為
revalidateなし
初回にAPIルートから新鮮なデータを取得し、クライアントで保持して使いまわすことで外部APIへのfetchを減らす。
*zustand
zustandはグローバル状態を作るためのライブラリ。素のContextと違いProviderが必要がない。
*今回のようなサーバーと直接接点があるケースはreact-queryが適している。今回の挙動確認はクライアントで保持するキャッシュの有効期限を操作することについては考えていない。
pnpm add zustandstore.ts:
import { create } from "zustand";
export const useTimeStore = create<any>((set, get) => ({
time: null,
fetchTime: async () => {
if (get().time) return; // すでに取得済みなら再fetchしない
try {
const res = await fetch("/api/time2");
const data = await res.json();
set({ time: data });
} catch (e) {
console.error("fetchTime error", e);
}
},
}));page.tsx:
export default function TimePage() {
const { time, fetchTime } = useTimeStore();
useEffect(() => {
fetchTime();
}, [fetchTime]);
return (
<main className="p-6">
{time && <div className="text-lg">Client time: {time.time}</div>}
</main>
);
}react-query
pnpm add @tanstack/react-queryprovider.tsx:
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
export function Providers({ children }: { children: React.ReactNode }) {
const queryClient = new QueryClient();
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}page.tsx :
export default function TimePage() {
const { data } = useQuery({
queryKey: ["time"],
queryFn: async () => {
const res = await fetch("/api/time");
return res.json();
},
});
return (
<main className="p-6">
<div className="text-lg">Server time: {data?.time ?? "Loading..."}</div>
</main>
);revalidateあり
"本来リロードしないと反映されない部分を別の手段で実現" する必要がある。
<button onClick={() => window.location.reload()}>refresh</button>強制的にリロードさせる。
const Page = () => {
const [time, setTime] = useState("");
useEffect(() => {
const load = async () => {
const res = await fetch("/api/time");
setTime((await res.json()).time);
setTimeout(async () => {
const res2 = await fetch("/api/time");
setTime((await res2.json()).time);
}, 1000);
};
load();
}, []);
return (
<div className="p-6 space-y-2">
<div>API response: {time}</div>
</div>
);
};
export default Page;useEffect内で2度fetchを行う。
const { data, refetch } = useQuery({
queryKey: ["time"],
queryFn: async () => {
const res = await fetch("/api/time");
return res.json();
},
});
useEffect(() => {
const timer = setTimeout(() => {
refetch();
}, 1000);
return () => clearTimeout(timer);
}, [refetch]);react-queryの場合
上記は1秒以内にNext.jsのISRが更新完了する前提じゃないと成立しない。