データ取得とキャッシュ — 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 関数にできます。そのため、useEffect や useState を使わず、コンポーネントの中で直接 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などを使うとページは動的レンダリングになる- クエリは
searchParams(await)、複数取得はPromise.allで並列化
次の章では、データ取得中の ローディングとエラー処理 を扱います。fetch を待つ間に何を見せるか、失敗したらどう表示するか——loading.tsx と error.tsx で宣言的に実装できます。