前回の記事でServer ComponentとClient Componentの違いを学びました。
今回はServer Componentの真価を発揮する場面、外部APIからデータを取得して表示する方法を学びます。
従来のReact(SPA)では、データ取得といえば useEffect の中で fetch を呼ぶのが定番でした。
Next.jsのServer Componentでは、その必要がありません。
コンポーネント自体を async 関数にして、直接 await fetch() を書くだけです。
Contents
基本:async Server Componentでデータを取得する
まず一番シンプルな例を見てみましょう。
今回は無料で使える JSONPlaceholder というダミーAPIを使います。
// app/posts/page.tsx
type Post = {
id: number;
title: string;
body: string;
};
export default async function PostsPage() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
const posts: Post[] = await res.json();
return (
<main>
<h1>投稿一覧</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>
<strong>{post.title}</strong>
</li>
))}
</ul>
</main>
);
}コンポーネントを async にして await fetch() を呼ぶだけ——これがServer Componentのデータ取得の基本形です。
サーバー上でデータを取得してからHTMLを生成するので、ブラウザには最初から完成した状態のページが届きます。
ローディング中の表示 — loading.tsx
データの取得には時間がかかります。
その間ユーザーに何も見せないのは体験が悪いですよね。
Next.jsでは loading.tsx というファイルを置くだけで、データ取得中のUIを表示できます。
app/
└── posts/
├── page.tsx ← データ取得・表示
└── loading.tsx ← 読み込み中に表示される// app/posts/loading.tsx
export default function Loading() {
return (
<main>
<h1>投稿一覧</h1>
<p>読み込み中...</p>
</main>
);
}page.tsx のデータ取得が完了するまでの間、自動的に loading.tsx の内容が表示されます。
スケルトンスクリーン(グレーのプレースホルダー)を使うとよりリッチな表現ができます。
// app/posts/loading.tsx(スケルトン版)
export default function Loading() {
return (
<main>
<h1>投稿一覧</h1>
<ul style={{ listStyle: 'none', padding: 0 }}>
{[...Array(5)].map((_, i) => (
<li
key={i}
style={{
height: '1.5rem',
background: '#e0e0e0',
borderRadius: '4px',
marginBottom: '0.75rem',
animation: 'pulse 1.5s ease-in-out infinite',
}}
/>
))}
</ul>
</main>
);
}エラーのハンドリング — error.tsx
ネットワークエラーやAPIのレスポンスが不正だった場合の表示も、専用ファイルで管理できます。
app/
└── posts/
├── page.tsx
├── loading.tsx
└── error.tsx ← エラー時に表示される// app/posts/error.tsx
"use client"; // error.tsx は必ず Client Component にする
import { useEffect } from 'react';
type Props = {
error: Error;
reset: () => void; // もう一度試すための関数
};
export default function Error({ error, reset }: Props) {
useEffect(() => {
console.error(error);
}, [error]);
return (
<main>
<h2>データの読み込みに失敗しました</h2>
<p>{error.message}</p>
<button onClick={reset}>もう一度試す</button>
</main>
);
}注意:
error.tsxは必ず"use client"にする必要があります。エラー情報の受け取りやリトライ処理にブラウザ側の動きが必要なためです。
試しに、app/posts/page.tsxのfetchで指定したurlに適当な一文字を加えるなどしてわざとエラーを出すと動作の確認ができます。
キャッシュ戦略 — データをいつ再取得するか
Next.jsの fetch は、ブラウザの fetch を拡張していて、キャッシュの制御ができます。
これがNext.jsのデータ取得を理解するうえで外せないポイントです。
デフォルト(キャッシュあり)
const res = await fetch('https://api.example.com/posts');
// デフォルトではビルド時にキャッシュされ、再取得しない毎回最新データを取得する
const res = await fetch('https://api.example.com/posts', {
cache: 'no-store',
});リアルタイム性が求められるデータ(在庫情報、最新ニュースなど)に使います。
一定時間ごとに再取得する
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 60 }, // 60秒ごとに再取得
});頻繁に変わらないが定期的に更新したいデータ(ブログ記事一覧など)に向いています。
⚠️ cacheオプションとnextオプション
Next.jsはWeb fetch()APIを拡張します。
nextオプションはnextjsの仕様でJavaSctiptの標準仕様では無いです。
Next.js以外では使えない可能性があります。
cacheオプションはJavascriptの標準仕様です。
使い分けの目安
| データの性質 | 設定 |
|---|---|
| ほぼ変わらない(会社概要など) | デフォルト(キャッシュ) |
| 定期的に更新される(ブログ記事など) | revalidate: N(秒) |
| 常に最新が必要(在庫・株価など) | cache: 'no-store' |
詳細ページでデータを取得する
動的ルート([id])のページでも同じようにデータを取得できます。
記事2で作ったブログ詳細ページに実際のデータ取得を組み合わせてみましょう。
// app/posts/[id]/page.tsx
import { notFound } from 'next/navigation';
type Post = {
id: number;
title: string;
body: string;
};
type Props = {
params: Promise<{ id: string }>;
};
export default async function PostPage({ params }: Props) {
const { id } = await params;
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
if (!res.ok) {
notFound();
}
const post: Post = await res.json();
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>
);
}res.ok が false(404や500など)のときに notFound() を呼ぶことで、記事#2で作った not-found.tsx のページが表示されます。
一覧ページから詳細ページへ繋げる
ここまでの内容を組み合わせて、一覧 → 詳細の流れを完成させましょう。
// app/posts/page.tsx
import Link from 'next/link';
type Post = {
id: number;
title: string;
body: string;
};
export default async function PostsPage() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
next: { revalidate: 60 },
});
const posts: Post[] = await res.json();
// 表示数を絞る
const recentPosts = posts.slice(0, 10);
return (
<main>
<h1>投稿一覧</h1>
<ul style={{ listStyle: 'none', padding: 0 }}>
{recentPosts.map((post) => (
<li key={post.id} style={{ marginBottom: '1rem', borderBottom: '1px solid #eee', paddingBottom: '1rem' }}>
<Link href={`/posts/${post.id}`}>
<strong>{post.title}</strong>
</Link>
<p style={{ color: '#666', marginTop: '0.25rem' }}>
{post.body.slice(0, 80)}...
</p>
</li>
))}
</ul>
</main>
);
}Suspenseで部分的なローディングを制御する
loading.tsx はページ全体のローディング表示でした。
ページの一部だけをローディング状態にしたい場合は、Reactの <Suspense> を使います。
たとえばトップページに「最新の投稿」セクションだけを非同期で読み込みたい場合:
// components/LatestPosts.tsx(Server Component)
type Post = { id: number; title: string };
export default async function LatestPosts() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=5', {
cache: "no-store",
});
const posts: Post[] = await res.json();
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}// app/page.tsx
import { Suspense } from 'react';
import LatestPosts from '@/components/LatestPosts';
export default function Home() {
return (
<main>
<h1>ようこそ</h1>
<p>このサイトはNext.jsで作られています。</p>
<h2>最新の投稿</h2>
<Suspense fallback={<p>読み込み中...</p>}>
<LatestPosts />
</Suspense>
</main>
);
}<Suspense> でラップしたコンポーネントのデータ取得が完了するまでの間、fallback に指定した内容が表示されます。
ページの他の部分はすでに表示されているので、ユーザーは待たされている感覚を受けにくくなります。
ここまでのフォルダ構成
app/
├── page.tsx ← トップページ(Suspense を使用)
└── posts/
├── page.tsx ← 投稿一覧(fetch + revalidate)
├── loading.tsx ← 読み込み中の表示
├── error.tsx ← エラー時の表示
└── [id]/
└── page.tsx ← 投稿詳細(動的データ取得)
components/
└── LatestPosts.tsx ← 部分的なデータ取得コンポーネントまとめ
この記事で学んだこと:
- Server Componentを
asyncにしてawait fetch()を書くだけでデータ取得できる loading.tsxを置くだけでローディング中のUIを表示できるerror.tsx("use client"必須)でエラー時のUIを管理できるfetchのcacheオプションでデータの再取得タイミングを制御できる<Suspense>でページの一部だけをローディング状態にできる
次の記事では、Next.jsのRoute Handlerを使ってAPIエンドポイント自体を作る方法を学びます。
「フロントエンドのコードだけでAPIも書ける」という感覚を体験しましょう。


























