ウェブエンジニア問題集

データ取得とキャッシュ — Server Componentのfetchとrevalidateの仕組み

Webアプリの本質は「データを取得して表示する」ことです。Next.js(App Router)では、Server Componentの中で fetch を直接 await するだけでデータが取れます。さらにNext.jsは fetch の結果を自動でキャッシュし、「いつ再取得するか」まで細かく制御できます。

この章では、Server Componentでのデータ取得の基本から、キャッシュと revalidate、静的・動的レンダリングの分かれ目までを押さえます。

学習者学習者

Reactだと useEffect の中で fetch してたけど、Next.jsだと書き方が違うって聞いて混乱してる…。

Server Componentでは fetch を直接 await する

第3章で見たとおり、App RouterのコンポーネントはデフォルトでServer Componentであり、async 関数にできます。そのため、useEffectuseState を使わず、コンポーネントの中で直接 await fetch(...) できます。

// src/app/posts/page.tsx — Server Component
export default async function PostsPage() {
  const res = await fetch("https://api.example.com/posts");
  const posts = await res.json();
 
  return (
    <ul>
      {posts.map((post: { id: number; title: string }) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}
先生先生

useEffect での取得は「描画してから取りに行く」。Server Componentは「サーバーで取り終えてから描画する」。だからローディングのちらつきが無く、SEOにも強いんだ。

サーバーでデータを取得して描画するイメージ

fetch のキャッシュと再検証(revalidate)

Next.jsは標準の fetch を拡張しており、取得結果をキャッシュします。どう振る舞わせるかは第2引数のオプションで指定します。

構文: fetch(url, options?)

オプション指定例挙動
指定なしfetch(url)デフォルトでキャッシュされる(同じURLは再取得しない)
cache: 'no-store'fetch(url, { cache: 'no-store' })毎回取得する(常に最新。キャッシュしない)
next: { revalidate: 秒数 }fetch(url, { next: { revalidate: 60 } })指定秒ごとに再取得(ISR:時間ベースの再検証)
next: { tags: [...] }fetch(url, { next: { tags: ['posts'] } })タグを付け、後から revalidateTag で再検証できる
// 60秒ごとに最新化(ニュース一覧などに最適)
const res = await fetch("https://api.example.com/posts", {
  next: { revalidate: 60 },
});
 
// 常に最新が必要(在庫・残高など)
const res2 = await fetch("https://api.example.com/stock", {
  cache: "no-store",
});
学習者学習者

キャッシュされるのは速くて嬉しいけど、「更新したのに古いまま」でハマりそう…。

まさにそこが要注意ポイントです。「データの鮮度」と「速度」はトレードオフです。ほぼ変わらないデータはキャッシュ(または長めの revalidate)、刻々と変わるデータは no-store、とデータの性質に合わせて選ぶのがコツです。

静的レンダリングと動的レンダリングの分かれ目

Next.jsは、ページを次のどちらかでレンダリングします。

  • 静的レンダリング:ビルド時にHTMLを生成(高速・CDN配信向き)
  • 動的レンダリング:リクエストごとにサーバーで生成(常に最新)

どちらになるかは自動で判定されます。おおまかな基準は次のとおりです。

こうすると…レンダリング
通常の fetch(キャッシュあり)静的
cache: 'no-store' を使う動的
cookies() / headers() を読む動的
searchParams を使う動的
flowchart TB
    A["ページ"] --> B{"動的な要素を使う?"}
    B -->|"no-store / cookies / searchParams"| C["動的レンダリング(毎回生成)"]
    B -->|"使わない"| D["静的レンダリング(ビルド時生成)"]

クエリ文字列を読む — searchParams

/search?q=nextjs のような ? 以降のクエリは、ページの searchParams から受け取ります。params と同様、Next.js 15以降は Promise なので await します。

// src/app/search/page.tsx
export default async function SearchPage({
  searchParams,
}: {
  searchParams: Promise<{ q?: string }>;
}) {
  const { q } = await searchParams;
  const res = await fetch(`https://api.example.com/search?q=${q ?? ""}`, {
    cache: "no-store",
  });
  const results = await res.json();
 
  return <p>「{q}」の検索結果: {results.length}件</p>;
}

なお searchParams を使うとそのページは動的レンダリングになります(検索結果はリクエストごとに変わるため自然な挙動です)。

複数のデータを効率よく取る — 並列取得

複数の fetch を順番に await すると、待ち時間が積み重なって遅くなります(ウォーターフォール)。同時に取れるものは Promise.all並列化しましょう。

// ❌ 直列:userを待ってからpostsを取る(合計=両者の和)
const user = await fetch("/api/user").then((r) => r.json());
const posts = await fetch("/api/posts").then((r) => r.json());
 
// ⭕ 並列:同時に投げて両方待つ(合計=遅い方だけ)
const [user2, posts2] = await Promise.all([
  fetch("/api/user").then((r) => r.json()),
  fetch("/api/posts").then((r) => r.json()),
]);

Promise.all の使い方は、JavaScriptの非同期処理(Promise)で詳しく扱っています。

まとめ

  • Server Componentでは useEffect を使わず、コンポーネント内で直接 await fetch
  • fetch の結果はデフォルトでキャッシュされる。cache: 'no-store' で毎回取得、next: { revalidate: 秒 } で定期再取得
  • 「鮮度」と「速度」はトレードオフ。データの性質に合わせて選ぶ
  • no-store / cookies / searchParams などを使うとページは動的レンダリングになる
  • クエリは searchParamsawait)、複数取得は Promise.all で並列化

次の章では、データ取得中の ローディングとエラー処理 を扱います。fetch を待つ間に何を見せるか、失敗したらどう表示するか——loading.tsxerror.tsx で宣言的に実装できます。