✨
【Next.js完全ガイド Vol.3】データフェッチング戦略の完全ガイド
【Next.js完全ガイド Vol.3】データフェッチング戦略の完全ガイド
はじめに
この記事は「Next.js完全ガイド」シリーズの第3回です。
📚 シリーズ構成
- 第1回: Next.js 15最新情報 & アーキテクチャの基礎
- 第2回: レンダリング戦略の完全理解
- 第3回: データフェッチング戦略 ← 今ここ
- 第4回: キャッシング戦略
- 第5回: エラーハンドリング・メタデータ・実践パターン
前回のおさらい
前回は、Next.jsの3つのレンダリング戦略を学びました:
- 静的レンダリング(ビルド時に生成)
- 動的レンダリング(リクエストごとに生成)
- ストリーミング(段階的に配信)
- コンポーネント単位での使い分け方法
この記事で学べること
- 4つのデータフェッチングパターン
- Next.js 15のキャッシング動作の重要な変更点
- Server Actions の使い方と実践例
- SWRを使った高度なクライアントサイドフェッチング
- データフェッチング戦略の選び方
1. データフェッチングの基本原則
Next.jsのデータフェッチングは、どこで(サーバー/クライアント)、いつ(ビルド時/リクエスト時)、どのように(キャッシュする/しない)取得するかを柔軟に制御できます。
4つの主要なパターン
┌─────────────────────────────────────────┐
│ 1. Server Components でのデータ取得 │
│ - デフォルトの方法 │
│ - データソースに近い │
│ - セキュア │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ 2. Route Handlers (API Routes) │
│ - /app/api/... │
│ - REST API エンドポイント │
│ - 外部クライアントとの統合 │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ 3. Server Actions │
│ - フォーム送信 │
│ - データ変更(mutation) │
│ - 'use server' ディレクティブ │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ 4. Client Components でのフェッチ │
│ - useEffect + fetch │
│ - SWR / React Query │
│ - リアルタイムデータ │
└─────────────────────────────────────────┘
2. Server Componentsでのデータフェッチング
基本パターン
// app/posts/page.js
// 最もシンプルなパターン
export default async function PostsPage() {
const posts = await fetch('https://api.example.com/posts')
.then(res => res.json());
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
Next.js 15のキャッシング動作
⚠️ 重要な変更点:
Next.js 15から、fetchリクエストはデフォルトでキャッシュされなくなりました。
// Next.js 14: 自動的にキャッシュされる
const data = await fetch('https://api.example.com/data');
// Next.js 15: キャッシュされない(毎回新しいデータを取得)
const data = await fetch('https://api.example.com/data');
// Next.js 15: 明示的にキャッシュする
const data = await fetch('https://api.example.com/data', {
cache: 'force-cache'
});
// 再検証付きキャッシュ(60秒ごとに更新)
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 60 }
});
パターン別の実装例
パターン1: リアルタイムデータ(キャッシュなし)
// 株価、為替レート、ライブスコアなど
export default async function StockPrice() {
const price = await fetch('https://api.stocks.com/AAPL', {
cache: 'no-store' // 明示的にキャッシュを無効化
}).then(r => r.json());
return <div>現在価格: ${price.current}</div>;
}
パターン2: 時間ベースの再検証(ISR)
// ニュース記事、ブログ投稿など
export default async function NewsPage() {
const articles = await fetch('https://api.news.com/articles', {
next: {
revalidate: 300 // 5分ごとに再検証
}
}).then(r => r.json());
return (
<div>
{articles.map(article => (
<Article key={article.id} {...article} />
))}
</div>
);
}
動作の流れ:
時刻: 10:00 - ユーザーA訪問
↓
キャッシュなし → API呼び出し → レンダリング → キャッシュ保存
時刻: 10:03 - ユーザーB訪問
↓
キャッシュあり(まだ5分経過していない) → 即座に表示
時刻: 10:06 - ユーザーC訪問
↓
キャッシュ期限切れ → 古いキャッシュ表示
↓
バックグラウンドで再検証開始
↓
次回アクセス時には新しいデータ
パターン3: 完全静的(ビルド時のみ取得)
// FAQページ、会社概要など
export default async function AboutPage() {
const companyInfo = await fetch('https://api.company.com/info', {
cache: 'force-cache' // ビルド時に取得し、永続的にキャッシュ
}).then(r => r.json());
return <div>{companyInfo.description}</div>;
}
パターン4: オンデマンド再検証(タグベース)
// app/products/page.js
export default async function ProductsPage() {
const products = await fetch('https://api.shop.com/products', {
next: {
tags: ['products'] // タグ付け
}
}).then(r => r.json());
return <ProductList products={products} />;
}
// 管理画面で商品を更新したとき
// app/api/revalidate/route.js
import { revalidateTag } from 'next/cache';
export async function POST(request) {
revalidateTag('products'); // 'products'タグのキャッシュを即座に無効化
return Response.json({ revalidated: true });
}
複数のデータソースからの並列取得
export default async function Dashboard() {
// 並列実行で高速化
const [user, posts, analytics] = await Promise.all([
fetch('https://api.example.com/user').then(r => r.json()),
fetch('https://api.example.com/posts').then(r => r.json()),
fetch('https://api.example.com/analytics').then(r => r.json())
]);
return (
<div>
<UserProfile user={user} />
<PostList posts={posts} />
<AnalyticsChart data={analytics} />
</div>
);
}
実行タイムライン:
【逐次実行】
User取得(500ms)
↓
Posts取得(800ms)
↓
Analytics取得(600ms)
↓
合計: 1900ms
【並列実行】Promise.all使用
User取得(500ms) ┐
Posts取得(800ms) ├─ 同時実行
Analytics取得(600ms)┘
↓
合計: 800ms(最も遅い処理の時間)
3. Route Handlers(API Routes)
基本構造
// app/api/posts/route.js
export async function GET(request) {
const posts = await db.posts.findMany();
return Response.json(posts);
}
export async function POST(request) {
const body = await request.json();
const post = await db.posts.create({ data: body });
return Response.json(post, { status: 201 });
}
動的ルート
// app/api/posts/[id]/route.js
export async function GET(request, { params }) {
const { id } = params;
const post = await db.posts.findUnique({ where: { id } });
if (!post) {
return Response.json({ error: 'Not found' }, { status: 404 });
}
return Response.json(post);
}
export async function PUT(request, { params }) {
const { id } = params;
const body = await request.json();
const post = await db.posts.update({
where: { id },
data: body
});
return Response.json(post);
}
export async function DELETE(request, { params }) {
const { id } = params;
await db.posts.delete({ where: { id } });
return Response.json({ message: 'Deleted' });
}
URLパラメータとヘッダーの扱い
// app/api/search/route.js
export async function GET(request) {
// URLパラメータ
const { searchParams } = new URL(request.url);
const query = searchParams.get('q');
const limit = searchParams.get('limit') || 10;
const page = searchParams.get('page') || 1;
// ヘッダー
const authToken = request.headers.get('authorization');
const userAgent = request.headers.get('user-agent');
// クッキー
const sessionId = request.cookies.get('session-id');
// 検証
if (!authToken) {
return Response.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
const results = await searchPosts(query, { limit, page });
return Response.json(results);
}
// 使用例: /api/search?q=nextjs&limit=5&page=1
ストリーミングレスポンス
// app/api/stream/route.js
export async function GET() {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
// 大量のデータを少しずつ送信
for (let i = 0; i < 10; i++) {
const data = await fetchChunk(i);
controller.enqueue(encoder.encode(JSON.stringify(data) + '\n'));
await new Promise(resolve => setTimeout(resolve, 100));
}
controller.close();
}
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache'
}
});
}
4. Server Actions
Server Actionsは、サーバー側の関数をクライアントから直接呼び出せるNext.js 13.4以降の機能です。API Routeを書かずにデータ変更ができます。
基本的な使い方
// app/actions.js
'use server'; // このファイル全体がServer Actions
export async function createPost(formData) {
const title = formData.get('title');
const content = formData.get('content');
const post = await db.posts.create({
data: { title, content }
});
revalidatePath('/posts'); // キャッシュを更新
return post;
}
// app/posts/new/page.js
import { createPost } from '@/app/actions';
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" placeholder="タイトル" required />
<textarea name="content" placeholder="本文" required />
<button type="submit">投稿</button>
</form>
);
}
動作の流れ:
1. ユーザーがフォームを送信
↓
2. Next.jsが自動的にServer Actionを呼び出し
↓
3. サーバーでcreatePost関数が実行
↓
4. データベースに保存
↓
5. revalidatePath()でキャッシュ更新
↓
6. ページが自動的に再レンダリング
Client Componentでの使用
// app/components/DeleteButton.js
'use client';
import { deletePost } from '@/app/actions';
import { useTransition } from 'react';
export default function DeleteButton({ postId }) {
const [isPending, startTransition] = useTransition();
const handleDelete = () => {
startTransition(async () => {
await deletePost(postId);
});
};
return (
<button onClick={handleDelete} disabled={isPending}>
{isPending ? '削除中...' : '削除'}
</button>
);
}
// app/actions.js
'use server';
export async function deletePost(postId) {
await db.posts.delete({ where: { id: postId } });
revalidatePath('/posts');
}
楽観的更新(Optimistic UI)
'use client';
import { likePost } from '@/app/actions';
import { useOptimistic } from 'react';
export default function LikeButton({ postId, initialLikes }) {
const [optimisticLikes, addOptimisticLike] = useOptimistic(
initialLikes,
(state, amount) => state + amount
);
const handleLike = async () => {
addOptimisticLike(1); // すぐにUIを更新
await likePost(postId); // バックグラウンドで実際のいいねを処理
};
return (
<button onClick={handleLike}>
❤️ {optimisticLikes}
</button>
);
}
体験の違い:
【楽観的更新なし】
クリック → 待つ... → サーバー完了 → 表示更新(遅く感じる)
【楽観的更新あり】
クリック → 即座に表示更新!→ バックグラウンドでサーバー処理
Server Actions vs API Routes
| 特徴 | Server Actions | API Routes |
|---|---|---|
| 使用目的 | フォーム送信、データ変更 | RESTful API、外部統合 |
| 記述量 | 少ない(関数のみ) | 多い(ルートファイル必要) |
| 型安全性 | 完全(TypeScript) | 手動で型定義 |
| プログレッシブエンハンスメント | あり(JSなしでも動作) | なし |
| エンドポイント公開 | なし | あり(/api/...) |
| 推奨用途 | 内部のデータ変更 | 外部クライアント向けAPI |
実践例:完全な投稿管理システム
// app/actions.js
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function createPost(prevState, formData) {
const title = formData.get('title');
const content = formData.get('content');
// バリデーション
if (!title || title.length < 3) {
return { error: 'タイトルは3文字以上必要です' };
}
if (!content || content.length < 10) {
return { error: '本文は10文字以上必要です' };
}
try {
const post = await db.posts.create({
data: { title, content }
});
revalidatePath('/posts');
redirect(`/posts/${post.id}`);
} catch (error) {
return { error: '投稿に失敗しました' };
}
}
export async function updatePost(postId, formData) {
const title = formData.get('title');
const content = formData.get('content');
await db.posts.update({
where: { id: postId },
data: { title, content }
});
revalidatePath(`/posts/${postId}`);
revalidatePath('/posts');
}
export async function deletePost(postId) {
await db.posts.delete({
where: { id: postId }
});
revalidatePath('/posts');
redirect('/posts');
}
// app/posts/new/page.js
'use client';
import { createPost } from '@/app/actions';
import { useFormState, useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? '投稿中...' : '投稿する'}
</button>
);
}
export default function NewPostPage() {
const [state, formAction] = useFormState(createPost, null);
return (
<form action={formAction}>
<div>
<label htmlFor="title">タイトル</label>
<input
id="title"
name="title"
placeholder="タイトルを入力"
required
/>
</div>
<div>
<label htmlFor="content">本文</label>
<textarea
id="content"
name="content"
placeholder="本文を入力"
required
rows={10}
/>
</div>
{state?.error && (
<p className="error">{state.error}</p>
)}
<SubmitButton />
</form>
);
}
5. Client Componentsでのデータフェッチング
useEffectを使った基本パターン
'use client';
import { useState, useEffect } from 'react';
export default function Posts() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch('/api/posts')
.then(r => {
if (!r.ok) throw new Error('Failed to fetch');
return r.json();
})
.then(data => {
setPosts(data);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, []);
if (loading) return <div>読み込み中...</div>;
if (error) return <div>エラー: {error}</div>;
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
SWRを使った高度なパターン
SWRは、Next.jsの開発元Vercelが作ったデータフェッチングライブラリです。
npm install swr
'use client';
import useSWR from 'swr';
const fetcher = (url) => fetch(url).then(r => r.json());
export default function Posts() {
const { data, error, isLoading, mutate } = useSWR('/api/posts', fetcher, {
refreshInterval: 3000, // 3秒ごとに自動更新
revalidateOnFocus: true // タブに戻ったときに再検証
});
if (error) return <div>エラーが発生しました</div>;
if (isLoading) return <div>読み込み中...</div>;
return (
<div>
<button onClick={() => mutate()}>更新</button>
<ul>
{data.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
SWRの主な機能
┌────────────────────────────────────┐
│ 1. 自動再検証 │
│ - フォーカス時 │
│ - 一定時間ごと │
│ - ネットワーク再接続時 │
└────────────────────────────────────┘
┌────────────────────────────────────┐
│ 2. キャッシュ管理 │
│ - 自動的にキャッシュ │
│ - 複数コンポーネント間で共有 │
└────────────────────────────────────┘
┌────────────────────────────────────┐
│ 3. Optimistic UI │
│ - 即座にUIを更新 │
│ - バックグラウンドで同期 │
└────────────────────────────────────┘
┌────────────────────────────────────┐
│ 4. エラーハンドリング │
│ - 自動リトライ │
│ - エラー状態の管理 │
└────────────────────────────────────┘
SWRでの楽観的更新
'use client';
import useSWR from 'swr';
const fetcher = (url) => fetch(url).then(r => r.json());
export default function TodoList() {
const { data: todos, mutate } = useSWR('/api/todos', fetcher);
const addTodo = async (text) => {
// 楽観的更新:UIを即座に更新
const newTodo = { id: Date.now(), text, completed: false };
mutate([...todos, newTodo], false); // false = 再検証しない
// サーバーに送信
try {
await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo)
});
// 成功したら再検証
mutate();
} catch (error) {
// 失敗したら元に戻す
mutate(todos);
}
};
return (
<div>
<button onClick={() => addTodo('新しいタスク')}>
追加
</button>
<ul>
{todos?.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</div>
);
}
6. データフェッチング戦略の選び方
判断フローチャート
┌────────────────────────────────────────┐
│ データの性質を考える │
└────────────────────────────────────────┘
↓
変更頻度は?
↓
┌────┴────┐
│ │
ほぼ変わらない 頻繁に変わる
│ │
↓ ↓
静的レンダリング 動的レンダリング
+ force-cache + no-store
│ │
│ ↓
│ 誰が必要とする?
│ │
│ ┌────┴────┐
│ │ │
│ サーバー クライアント
│ │ │
│ ↓ ↓
│ Server Client
│ Components Components
│ + fetch + SWR
│
└─→ ユーザー固有?
│
┌────┴────┐
│ │
YES NO
│ │
↓ ↓
ISR with ISR
tags (時間ベース)
具体的な判断フロー
Q: ブログ記事を表示したい
↓
A1: 記事は頻繁に更新される?
→ いいえ → 静的レンダリング + ISR (revalidate: 3600)
Q: ユーザーダッシュボードを作りたい
↓
A1: ユーザーごとに違うデータ?
→ はい
A2: リアルタイム性が重要?
→ はい → Client Components + SWR
→ いいえ → Server Components + 動的レンダリング
Q: コメント機能を追加したい
↓
A1: 追加後すぐに反映したい?
→ はい → Server Actions + revalidatePath
A2: 楽観的更新が必要?
→ はい → useOptimistic も追加
Q: 商品一覧を表示したい
↓
A1: 在庫や価格が変動する?
→ はい
A2: 管理画面で更新したら即反映?
→ はい → ISR + revalidateTag
→ いいえ → ISR (revalidate: 60)
パターン別の推奨実装
| ユースケース | 推奨手法 | 理由 |
|---|---|---|
| 企業サイト | Server Components + force-cache | ほぼ変わらない |
| ブログ | Server Components + ISR | たまに更新 |
| ECサイト商品 | Server Components + タグISR | 管理画面で更新時に反映 |
| ユーザー設定 | Server Components + 動的 | ユーザー固有 |
| ダッシュボード | Client Components + SWR | リアルタイム |
| チャット | Client Components + WebSocket | 双方向通信 |
| フォーム送信 | Server Actions | データ変更 |
| 外部API | Route Handlers | 外部統合 |
まとめ
この記事では、Next.jsのデータフェッチング戦略を詳しく学びました:
- ✅ Server Componentsでのfetch: 4つのキャッシングパターン
- ✅ Route Handlers: REST APIエンドポイントの実装
- ✅ Server Actions: フォーム送信とデータ変更
- ✅ Client Components + SWR: 高度なクライアントサイドフェッチング
- ✅ データフェッチング戦略の選び方
次回は「キャッシング戦略」について解説します。Next.jsの4つのキャッシュ層を完全に理解し、パフォーマンスを最大化する方法を学びます。
お楽しみに!
シリーズ記事
- 第1回: Next.js 15最新情報 & アーキテクチャの基礎
- 第2回: レンダリング戦略の完全理解
- 第3回: データフェッチング戦略 ← 今ここ
- 第4回: キャッシング戦略
- 第5回: エラーハンドリング・メタデータ・実践パターン
Discussion