レビュー済み·難易度: 中級·更新: 2026-04-14

Next.js 15 Server Actions でフォーム送信を実装する

Next.js App Router の Server Actions を使ってフォーム送信を実装する例。useActionState と Zod によるサーバー側バリデーションを示す。

nextjsformserver-actionsvalidationzodtailwindcss

対応バージョン

nextjs 15react 19typescript 5zod 3tailwindcss 4

前提環境

Next.js App Router と Server Components の基本を理解していること

概要

Server Actions を使うとフォーム送信を API Route なしでサーバー側で処理できる。 useActionState(旧 useFormState)と組み合わせることで、サーバーエラーをクライアントに返す型安全なパターンを実現する。

Action 定義

// src/app/contact/_actions/submit.ts
"use server";

import { z } from "zod";

const schema = z.object({
  name: z.string().min(1, "名前は必須です"),
  message: z.string().min(10, "メッセージは 10 文字以上で入力してください"),
});

export type ActionState = {
  errors?: Record<string, string[]>;
  success?: boolean;
};

export async function submitContact(
  _prev: ActionState,
  formData: FormData
): Promise<ActionState> {
  const result = schema.safeParse({
    name: formData.get("name"),
    message: formData.get("message"),
  });

  if (!result.success) {
    return { errors: result.error.flatten().fieldErrors };
  }

  // TODO: DB 保存や送信処理
  console.log("Received:", result.data);

  return { success: true };
}

フォームコンポーネント

// src/app/contact/page.tsx
"use client";

import { useActionState } from "react";
import { submitContact, type ActionState } from "./_actions/submit";

const initialState: ActionState = {};

export default function ContactPage() {
  const [state, action, isPending] = useActionState(submitContact, initialState);

  if (state.success) {
    return <p className="text-green-600">送信が完了しました。</p>;
  }

  return (
    <form action={action} className="space-y-4 max-w-sm">
      <div>
        <label htmlFor="name" className="block text-sm font-medium">
          名前
        </label>
        <input
          id="name"
          name="name"
          className="mt-1 block w-full rounded border px-3 py-2 text-sm"
        />
        {state.errors?.name && (
          <p className="mt-1 text-xs text-red-600">{state.errors.name[0]}</p>
        )}
      </div>

      <div>
        <label htmlFor="message" className="block text-sm font-medium">
          メッセージ
        </label>
        <textarea
          id="message"
          name="message"
          rows={4}
          className="mt-1 block w-full rounded border px-3 py-2 text-sm"
        />
        {state.errors?.message && (
          <p className="mt-1 text-xs text-red-600">{state.errors.message[0]}</p>
        )}
      </div>

      <button
        type="submit"
        disabled={isPending}
        className="w-full rounded bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
      >
        {isPending ? "送信中..." : "送信"}
      </button>
    </form>
  );
}

ポイント

  • "use server" ディレクティブで Server Action を定義する
  • useActionState でアクションの状態(エラー / 成功)を管理する
  • Zod の flatten().fieldErrors でフィールド単位のエラーを取得する

関連サンプル