Next.js

データフェッチング

Next.js

Server Components・fetch・キャッシュ

Server Components とデータフェッチ

非同期 Server Component での fetch

app/users/page.tsx tsx
// 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

actions.ts tsx
'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

フォームやミューテーションをサーバーで処理

actions-form.tsx tsx
// 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>;
}