概要
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]formのaction属性にformActionを渡すだけで送信が繋がる(API Route 不要)- バリデーションエラーは戻り値の
errorsに詰めてクライアントに返す isPendingがtrueの間はボタンをdisabledにして二重送信を防ぐ