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

Next.js Server Actions でフォーム送信を処理する

Server Actions と useActionState を使い、クライアントコンポーネントからサーバー関数を直接呼び出してフォーム送信を処理する実装例。

nextjsformserver-actions

対応バージョン

nextjs 15react 19

前提環境

Next.js App Router のサーバーコンポーネントとクライアントコンポーネントの違いを理解していること

概要

Server Actions を使うと、API Route を作らずにフォーム送信をサーバーで直接処理できる。 useActionState(React 19)と組み合わせることで、送信状態・バリデーションエラー・成功メッセージを宣言的に管理できる。

インストール

# 追加インストールは不要

実装例

// src/app/contact/actions.ts
"use server";

type FormState = {
  message: string;
  errors?: { name?: string; email?: string };
};

export async function submitContact(
  _prevState: FormState,
  formData: FormData
): Promise<FormState> {
  const name = formData.get("name") as string;
  const email = formData.get("email") as string;

  const errors: FormState["errors"] = {};
  if (!name || name.trim() === "") errors.name = "名前を入力してください";
  if (!email || !email.includes("@")) errors.email = "正しいメールアドレスを入力してください";

  if (Object.keys(errors).length > 0) {
    return { message: "", errors };
  }

  // TODO: DB 保存や外部 API 呼び出しをここで行う
  console.log("受信:", { name, email });

  return { message: `${name} さん、お問い合わせを受け付けました。` };
}
// src/app/contact/ContactForm.tsx
"use client";

import { useActionState } from "react";
import { submitContact } from "./actions";

const initialState = { message: "", errors: {} };

export function ContactForm() {
  const [state, formAction, isPending] = useActionState(submitContact, initialState);

  return (
    <form action={formAction} className="space-y-4">
      <div>
        <label htmlFor="name" className="block text-sm font-medium">
          名前
        </label>
        <input
          id="name"
          name="name"
          type="text"
          className="mt-1 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}</p>
        )}
      </div>

      <div>
        <label htmlFor="email" className="block text-sm font-medium">
          メールアドレス
        </label>
        <input
          id="email"
          name="email"
          type="email"
          className="mt-1 w-full rounded border px-3 py-2 text-sm"
        />
        {state.errors?.email && (
          <p className="mt-1 text-xs text-red-600">{state.errors.email}</p>
        )}
      </div>

      <button
        type="submit"
        disabled={isPending}
        className="rounded bg-blue-600 px-4 py-2 text-sm text-white disabled:opacity-50"
      >
        {isPending ? "送信中..." : "送信"}
      </button>

      {state.message && (
        <p className="text-sm text-green-600">{state.message}</p>
      )}
    </form>
  );
}
// src/app/contact/page.tsx
import { ContactForm } from "./ContactForm";

export default function ContactPage() {
  return (
    <main className="mx-auto max-w-md p-6">
      <h1 className="mb-4 text-lg font-bold">お問い合わせ</h1>
      <ContactForm />
    </main>
  );
}

ポイント

  • Server Actions は "use server" ディレクティブを付けた非同期関数で定義する
  • useActionState の第1引数に Server Action、第2引数に初期状態を渡す。戻り値は [state, formAction, isPending]
  • formaction 属性に formAction を渡すだけで送信が繋がる(API Route 不要)
  • バリデーションエラーは戻り値の errors に詰めてクライアントに返す
  • isPendingtrue の間はボタンを disabled にして二重送信を防ぐ

注意点

useActionState は React 19 から追加された。Next.js 15 以前では useFormState(react-dom)を使う。Server Actions は 'use server' ディレクティブで定義する。バリデーションはサーバー側で行うことが推奨。

関連サンプル