Nextjs:revalidate
2026年6月23日const res = await fetch(url, {
next: { revalidate: 500 },
});仕組み
revalidateに指定した秒数間はキャッシュされたデータが返る。Next.jsの
revalidateによるキャッシュは「Vercel の Data Cache」に保存される。CloudflareやVPSのデプロイでも機能する。
秒数経過後のリクエスト時にバックグラウンドでデータの再生成が行われる。= リクエストという「データ更新のためのトリガー」となる初回リクエストは古いデータが表示されるので犠牲になると言える。
つまりリクエストが少ないサイトだと
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をしないようにする。B:
revalidateを使い、本来リロードしないと反映されない部分を別の手段で実現する。
挙動を調べた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
クライアントで生成されてクライアントで保持するデータの状態管理に適している。
swr / tanstack-react-query
サーバーのデータをクライアントで保持するのに適している、なぜならfetch関連の細かい設定が可能な為
ベストプラクティス:データの生成元によりzustandとreact-queryを使い分ける。swrはreact-queryでも同じ事ができるのでreact-queryに慣れるほうがいい。
純粋なクライアントデータ(状態):zustand
サーバーからのデータ:tanstack-react-query
revalidateなし
初回にAPIルートから新鮮なデータを取得し、クライアントで保持して使いまわすことで外部APIへのfetchを減らす。
zustand_
zustandはグローバル状態を作るためのライブラリ。素のContextと違いProviderが必要がない。
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>
);
}swr_
pnpm add swrpage.tsx:
const fetcher = (url: string) => fetch(url).then((r) => r.json());
export default function TimePage() {
const { data } = useSWR("/api/time", fetcher);
return (
<main className="p-6">
<div className="text-lg">Server time: {data?.time ?? "Loading..."}</div>
</main>
);
}react-query_
pnpm add @tanstack/react-queryprovider.tsx:Layou.tsx等でimport
"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あり
バックグラウンドで再生成されたデータを初回のリクエストでUIへ反映させるために、クライアント側でAPIへの再fetchをすることで実現
swr
const { data , mutate } = useSWR("/api/time", fetcher);
useEffect(() => {
mutate();
}, [mutate]);react-query
import { useQuery } from '@tanstack/react-query';
export default function TimePage() {
const { data } = useQuery({
queryKey: ['time'],
queryFn: async () => {
const res = await fetch('/api/time');
return res.json();
},
// ページを開いた瞬間に、キャッシュがあろうがなかろうが必ず裏で1回フェッチを走らせる
refetchOnMount: 'always',
// 画面がフォーカスされた時も裏で更新する
refetchOnWindowFocus: true,
});
return (
<main className="p-6">
<div className="text-lg">Server time: {data?.time ?? 'Loading...'}</div>
</main>
);
}