0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

useActionState × Server Actions でつくるフォーム処理(fetch不要)

Posted at

はじめに

React 19 で登場した useActionState と Next.js の Server Actions を組み合わせることで、フォーム送信の「入力チェック → 重複チェック → DB 登録 → 完了ページへの移動」を一貫した形でシンプルに実装できます。

本記事では、useActionState と Server Actions の使い方を解説しながら、「お問い合わせフォーム」を作成していきます。

使用する技術は以下の通りです。

  • Next.js(App Router)
  • TypeScript
  • Prisma
  • Zod

useActionState とは

React 19 で導入された useActionState は、サーバーアクション関数と初期状態を渡すだけで、

  • フォーム送信結果に基づいて自動的に更新される state
  • そのアクションをフォームに渡すための formAction
  • 送信中かどうかを示す isPending

の3つをまとめて扱える React フックです。

const [state, formAction, isPending] = useActionState(actionFn, initialState);
  • state:最後に返された送信結果(成功・エラー・入力値など)
  • formAction:フォームのaction属性に渡す関数
  • isPending:送信中かどうかの真偽値(送信中はtrue
  • actionFn:フォームが送信されたときに呼び出される関数
  • initialState:フォームが初期化されたときの状態(state の初期値)

Server Actions とは

Next.js の Server Actions は、関数冒頭に 'use server' を宣言した非同期関数として定義し、バリデーションやデータベース操作などのサーバー側処理をシリアライズ可能な引数・戻り値で実装できる仕組みです。

クライアントコンポーネントでは、フォームの action 属性にその関数を渡すだけで、自動的に POST リクエストとして呼び出せます。

定義方法

'use server'ディレクティブを付与した非同期関数として定義します。
この関数をエクスポートすると、自動的に Server Action として扱われます。

"use server";
export const submitForm = async (
  previousState: ActionStateType,
  formData: FormData
): Promise<ActionStateType> => {
  // ここに入力チェックやDB操作などを書く
};

submitForm 関数を useActionState の第一引数に設定すると、前回の状態(previousState)と FormData を引数として受け取り、返り値として新しい状態をシリアライズ可能な形(JSON に変換できる構造)で返します。

呼び出し方法

クライアントコンポーネント内の<form action={formAction}>から呼び出せます。
従来のfetchや API ルートを経由する必要がないため、フォーム送信やデータ更新などのサーバー処理を Next.js アプリケーション内で完結させることができます。


Server Actions でお問い合わせフォームを作る

useActionStateと Server Actions を組み合わせて、お問い合わせフォームを作成します。
プロジェクトの構成は以下の通りです。

src/
├── app/
│   ├── complete/
│   │   └── page.tsx    # 完了ページ
│   └── contact/
│       └── page.tsx    # ContactFormを呼び出す
├── components/
│   └── ContactForm.tsx # お問い合わせフォーム
├── lib/
│   ├── actions/
│   │   └── submitForm.ts # Server Actions
│   └── prisma.ts         # Prisma Client
└── validations/
    └── contact.ts        # Zod バリデーション

ContactForm テーブルの構成は以下のとおりです。

カラム名 データ型 制約
id String 主キー、デフォルト値あり
userName String
mail String 重複不可
message String
createdAt DateTime デフォルト値あり

本記事では Prisma のインストールや使い方については触れませんので、Prisma が初めての方は他のリソースまたは以下の記事をご参照ください。

1. ContactForm コンポーネント

@/components/ContactForm.tsx
"use client";

import { ActionStateType, submitForm } from "@/lib/actions/submitForm";
import { useActionState } from "react";

const ContactForm = () => {
  const [state, formAction, isPending] = useActionState<
    ActionStateType,
    FormData
  >(submitForm, {
    success: true,
    userName: "",
    mail: "",
  });

  return (
    <form
      className="flex items-center justify-center min-h-screen"
      action={formAction}
    >
      <div className="w-auto h-auto flex flex-col gap-4 md:w-100">
        <h3>お問い合わせフォーム</h3>
        <div>
          <input
            type="text"
            id="userName"
            name="userName"
            placeholder="ユーザー名"
            defaultValue={state.userName}
            className="input-base"
          />
          {state.error?.userName && (
            <p className="error-text">{state.error.userName}</p>
          )}
        </div>
        <div>
          <input
            type="text"
            id="mail"
            name="mail"
            placeholder="メールアドレス"
            defaultValue={state.mail}
            className="input-base"
          />
          {state.error?.mail && (
            <p className="error-text">{state.error.mail}</p>
          )}
        </div>
        <textarea
          name="message"
          id="message"
          placeholder="メッセージを入力してください。"
          defaultValue={state.message}
          className="h-40 input-base"
        ></textarea>
        <button type="submit" disabled={isPending} className="submit-button">
          {isPending ? "送信中..." : "送信"}
        </button>
      </div>
    </form>
  );
};
  • useActionState(submitForm, {...})submitFormアクションと初期状態を紐付けします。
  • stateにはsubmitFormが返すActionStateTypeが常に最新で保持されます。
  • isPendingtrueの間は、ボタンを非活性にし「送信中...」と表示します。
  • <form action={formAction}>により、ブラウザの標準フォーム送信で Server Action が呼び出されます。

2. Zod バリデーション

ZodはTypeScriptと相性の良いスキーマ宣言・バリデーションライブラリで、シンプルなコードで型安全なバリデーションが可能です。

以下のコードでは、userNamemail の2つのフィールドについて、必須チェックと形式・文字数制限を定義しています。

@/validations/contact.ts
export const schema = z.object({
  userName: z
    .string()
    .min(1, "氏名は必須です")
    .max(20, "氏名は20文字以内で入力してください"),
  mail: z
    .string()
    .min(1, "メールアドレスは必須です")
    .email("メールアドレスを正しい形式で入力してください"),
});

スキーマ定義は以下の通りです。

  • .string():文字列ベースであること
  • .min(1, "必須エラー"):1文字以上であること
  • .max(20, "20文字以内で入力してください"):20文字以下であること
  • .email("メールアドレスを正しい形式で入力してください"):メールアドレスの形式であること

3. submitForm サーバーアクション

"use server";

import { redirect } from "next/navigation";
import { schema } from "@/validations/contact";
import { prisma } from "../prisma";

export type ActionStateType = {
  success: boolean;
  userName: string;
  mail: string;
  message?: string;
  error?: {
    userName?: string[];
    mail?: string[];
  };
};

export const submitForm = async (
  prev: ActionStateType,
  formData: FormData
): Promise<ActionStateType> => {
  const userName = formData.get("userName") as string;
  const mail = formData.get("mail") as string;
  const message = formData.get("message") as string;

  // 1) 入力チェック
  const validate = schema.safeParse({ userName, mail });
  if (!validate.success) {
    return {
      success: false,
      userName,
      mail,
      message,
      error: {
        userName: validate.error.flatten().fieldErrors.userName,
        mail: validate.error.flatten().fieldErrors.mail,
      },
    };
  }

  // 2) メール重複チェック(Prisma Client)
  const existRecord = await prisma.contactForm.findUnique({
    where: { mail },
  });
  if (existRecord) {
    return {
      success: false,
      userName,
      mail,
      message,
      error: {
        mail: ["このメールアドレスは既に使用されています"],
      },
    };
  }

  // 3) DB登録
  await prisma.contactForm.create({
    data: { userName, mail, message },
  });

  // 4) 完了ページへリダイレクト
  redirect("/complete");
};
  1. schema.safeParse({ userName, mail })で入力チェックを行い、success が false の時はerror.flatten().fieldErrorsでエラー一覧を取得できます。
  2. prisma.contactForm.findUnique({ where: { mail } })で重複チェックします。nullであれば未登録、オブジェクトであれば登録済みとなります。
  3. 問題なければcreate()で新規登録します。
  4. 最後にredirect("/complete")で完了ページへリダイレクトさせます。

まとめ

以上のように、useActionStateと Server Actions を組み合わせることで、複雑なfetchコードを書かずに、クライアント/サーバー間のフォーム送信ロジックを一貫して管理できます。

本記事が、 Next.js での効率的なフォーム処理の設計において、何か一つでもヒントになれば嬉しく思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?