データフェッチング
Next.js
Server Components・fetch・キャッシュ
Server Components とデータフェッチ
非同期 Server Component での fetch
// Server Component(デフォルト)— async が使える
export default async function UsersPage() {
// サーバー側で直接 fetch(クライアントに API キーを露出しない)
const res = await fetch('https://api.example.com/users', {
// キャッシュ制御
cache: 'force-cache', // SSG 相当(デフォルト)
// cache: 'no-store', // SSR 相当(毎リクエスト取得)
next: { revalidate: 3600 }, // ISR 相当(1時間ごとに再検証)
next: { tags: ['users'] }, // タグによる On-Demand Revalidation
});
if (!res.ok) throw new Error('取得失敗');
const users: User[] = await res.json();
// DB に直接アクセスも可能(クライアントには送られない)
// const users = await db.user.findMany();
return (
<ul>
{users.map(u => <li key={u.id}>{u.name}</li>)}
</ul>
);
}
// 並列フェッチ(Promise.all で高速化)
export default async function Dashboard() {
const [users, posts, stats] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/stats').then(r => r.json()),
]);
return <DashboardView users={users} posts={posts} stats={stats} />;
}キャッシュと再検証
revalidatePath・revalidateTag・unstable_cache
'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
// パスの再検証
async function createPost(data: FormData) {
await db.post.create({ data: { title: data.get('title') as string } });
revalidatePath('/blog'); // /blog を再検証
revalidatePath('/blog/[slug]', 'page'); // 動的ルート全て
}
// タグの再検証
async function updateUser(id: number, data: Partial<User>) {
await db.user.update({ where: { id }, data });
revalidateTag('users'); // 'users' タグを持つキャッシュを全て無効化
}
// unstable_cache: 任意の関数結果をキャッシュ
import { unstable_cache } from 'next/cache';
const getCachedUser = unstable_cache(
async (id: number) => db.user.findUnique({ where: { id } }),
['user'], // キャッシュキー
{
revalidate: 3600, // 1時間
tags: ['users'], // revalidateTag で無効化可能
}
);
// 使用
const user = await getCachedUser(1);Server Actions
フォームやミューテーションをサーバーで処理
// app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
// バリデーション
if (!title) return { error: 'タイトルは必須です' };
await db.post.create({ data: { title, content } });
revalidatePath('/blog');
redirect('/blog');
}
// app/blog/new/page.tsx
import { createPost } from '../actions';
export default function NewPost() {
return (
// action に Server Action を渡すだけ
<form action={createPost}>
<input name="title" placeholder="タイトル" required />
<textarea name="content" />
<SubmitButton />
</form>
);
}
// useFormStatus でペンディング状態
'use client';
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? '送信中...' : '投稿する'}</button>;
}