ファランクスブログ

© 2026 all rights reserved.
  1. blog
  2. next-revalidate
  • 仕組み
  • HTML or データ
  • useEffect
  • revalidatePath
  • 最新のデータを表示
  • ライブラリ
  • revalidateなし
  • zustand_
  • swr_
  • react-query_
  • revalidateあり
  • swr
  • react-query
version: 16

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 zustand

store.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 swr

page.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-query

provider.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>
  );
}

nextjs
/
cache
  • 仕組み
  • HTML or データ
  • useEffect
  • revalidatePath
  • 最新のデータを表示
  • ライブラリ
  • revalidateなし
  • zustand_
  • swr_
  • react-query_
  • revalidateあり
  • swr
  • react-query