Chuyển đến nội dung chính

レッスン 11: サーバー アクションとミューテーション

サーバーアクション、「サーバーを使用」。フォームアクション、段階的な強化。楽観的な更新、ActionState を使用します。エラー処理、変更後の再検証。

💻 プログラミング — レッスン 11 レッスン 11: サーバー アクションとミューテーション

React と Next.js: 基本から高度まで

パート 3: Next.js の基礎

xdev.asia

1. サーバーアクションとは何ですか?

サーバー アクションは、サーバー上で実行される非同期関数であり、フォームまたはイベント ハンドラーを介してクライアントから呼び出されます。ミューテーションの API ルートを置き換えます。

// actions.ts
'use server';

import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { db } from '@/lib/db';

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;

  await db.post.create({
    data: { title, content },
  });

  revalidatePath('/posts');
  redirect('/posts');
}

2. フォームアクション

// Server Component — no JS needed on client
import { createPost } from './actions';

export default function NewPostPage() {
  return (
    <form action={createPost}>
      <input name="title" required placeholder="Tiêu đề" />
      <textarea name="content" required placeholder="Nội dung" />
      <button type="submit">Tạo bài viết</button>
    </form>
  );
}

3. useActionState

'use client';
import { useActionState } from 'react';
import { createPost } from './actions';

type State = {
  errors?: { title?: string; content?: string };
  message?: string;
};

export function PostForm() {
  const [state, formAction, isPending] = useActionState<State, FormData>(
    createPost,
    { errors: {}, message: '' }
  );

  return (
    <form action={formAction}>
      <input name="title" />
      {state.errors?.title && <p className="text-red-500">{state.errors.title}</p>}

      <textarea name="content" />
      {state.errors?.content && <p className="text-red-500">{state.errors.content}</p>}

      <button type="submit" disabled={isPending}>
        {isPending ? 'Đang tạo...' : 'Tạo bài viết'}
      </button>

      {state.message && <p>{state.message}</p>}
    </form>
  );
}

4. Zodによる検証

// actions.ts
'use server';

import { z } from 'zod';
import { revalidatePath } from 'next/cache';

const PostSchema = z.object({
  title: z.string().min(3, 'Tiêu đề ít nhất 3 ký tự').max(100),
  content: z.string().min(10, 'Nội dung ít nhất 10 ký tự'),
});

export async function createPost(prevState: State, formData: FormData) {
  const parsed = PostSchema.safeParse({
    title: formData.get('title'),
    content: formData.get('content'),
  });

  if (!parsed.success) {
    return {
      errors: parsed.error.flatten().fieldErrors,
      message: 'Validation failed',
    };
  }

  try {
    await db.post.create({ data: parsed.data });
    revalidatePath('/posts');
    return { message: 'Tạo thành công!' };
  } catch {
    return { message: 'Có lỗi xảy ra' };
  }
}

5. 楽観的なアップデート

'use client';
import { useOptimistic } from 'react';
import { toggleLike } from './actions';

export function LikeButton({ liked, count }: { liked: boolean; count: number }) {
  const [optimistic, addOptimistic] = useOptimistic(
    { liked, count },
    (state, newLiked: boolean) => ({
      liked: newLiked,
      count: newLiked ? state.count + 1 : state.count - 1,
    })
  );

  return (
    <form action={async () => {
      addOptimistic(!optimistic.liked);
      await toggleLike();
    }}>
      <button type="submit">
        {optimistic.liked ? '❤️' : '🤍'} {optimistic.count}
      </button>
    </form>
  );
}

6. 削除およびインラインアクション

// Inline server action with bind
import { deletePost } from './actions';

export default async function PostList() {
  const posts = await db.post.findMany();

  return (
    <ul>
      {posts.map(post => {
        const deleteWithId = deletePost.bind(null, post.id);
        return (
          <li key={post.id}>
            {post.title}
            <form action={deleteWithId}>
              <button type="submit">Xóa</button>
            </form>
          </li>
        );
      })}
    </ul>
  );
}

7. サーバーアクションと API ルートの比較

特長サーバーアクションAPIルート
段階的な強化✅ JSなしで動作します❌ JS が必要です
タイプセーフティ✅ エンドツーエンド⚠️マニュアル
再検証✅ 内蔵⚠️マニュアル
ユースケースUIからの突然変異外部API、Webhook

次の記事: NextAuth.js による認証 — ログイン、認可、セッション管理。