⚛️

Conform + Server Actions の紹介と導入【Next.js】

に公開

はじめに

先日、React の勉強会で、フォーム実装について取り上げました 📝

フォーム処理とバリデーションは、Web アプリケーション開発において重要でありながら、意外と複雑になりがちな要素です。

今回は、Conform と Server Actions について調査したので、基礎的な内容をまとめました!

時間の節約になれば、嬉しいです 🙌

Conform とは?

https://github.com/edmundhung/conform

Conform は、Web の基本原則を活用した型安全なフォームバリデーションライブラリです

Conform の特徴は、HTML Forms を「プログレッシブエンハンスメント」の手法で強化し、
Remix や Next.js などのサーバーフレームワークと完全に連携できる点にあります。

主な特徴は以下の通りです:

  • 型安全:TypeScript との優れた連携
  • プログレッシブエンハンスメント:JavaScript が無効でも機能するフォーム設計
  • サーバーフレームワークとの連携:Next.js や Remix などと簡単に統合
  • DOM ベースの状態管理:FormData Web API を使用して DOM から直接フォームの値を取得
  • 柔軟な HTML 構造:フォームのマークアップに制限を設けない

Actions とは?

https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations

Actions(以前は Server Actions と呼ばれていました)は Next.js に組み込まれた、サーバーサイドでの処理を行うための機能です

フォームの送信やデータの更新(ミューテーション)を効率的に処理するための仕組みで、React 19 で安定版となった「Actions」機能を利用しています。

Actions の主な特徴は以下の通りです:

  • サーバーサイドの処理:クライアントからサーバーへ直接アクションを呼び出し可能
  • API エンドポイント不要:明示的な API ルートを作成せずにデータ操作が可能
  • プログレッシブエンハンスメント:JavaScript が読み込まれていない状態でもフォームが機能

Conform の基本的な使い方

Conform は単純明快な API を提供しています。

基本的な使い方を、確認してみます!!

基本的なコンセプト

https://ja.conform.guide/api/react/useForm

Conform の中心となるのは useForm フックです

このフックは、以下の主要な機能を提供します:

  • フォーム状態の管理
  • バリデーション処理
  • フィールドの自動生成
  • エラーハンドリング

以下は、基本的な Conform の使用例です:

import { useForm } from "@conform-to/react";
import { parseWithZod } from "@conform-to/zod";
import { z } from "zod";

// バリデーションスキーマの定義
const schema = z.object({
  username: z.string().min(2, "ユーザー名は2文字以上で入力してください"),
  email: z.string().email("有効なメールアドレスを入力してください"),
});

function LoginForm() {
  // useForm フックの使用
  const [form, fields] = useForm({
    // バリデーション処理の定義
    onValidate({ formData }) {
      return parseWithZod(formData, { schema });
    },
  });

  return (
    <form id={form.id} onSubmit={form.onSubmit}>
      <div>{form.errors}</div>

      <div>
        <label>ユーザー名</label>
        <input name={fields.username.name} />
        <div>{fields.username.errors}</div>
      </div>

      <div>
        <label>メールアドレス</label>
        <input type="email" name={fields.email.name} />
        <div>{fields.email.errors}</div>
      </div>

      <button>ログイン</button>
    </form>
  );
}

この例では、Conform の基本的な機能を使用しています:

  • useForm フックでフォームを初期化
  • parseWithZod を使って Zod スキーマと連携
  • fields オブジェクトを使って各フィールドの名前やエラーにアクセス
  • form.onSubmit でフォーム送信処理をハンドリング

Conform の導入手順(Next.js)

それでは、Conform と Server Actions を組み合わせたフォーム実装の手順を見ていきましょう。

1. ライブラリのインストール

npm install @conform-to/react @conform-to/zod zod

これにより、以下のライブラリがインストールされます:

  • @conform-to/react: React 用の Conform コアライブラリ
  • @conform-to/zod: Conform と Zod を連携させるアダプター
  • zod: バリデーションスキーマライブラリ

2. バリデーションスキーマの定義

まず、Zod を使ってバリデーションルールを定義します:

// schema.ts
import { z } from "zod";

export const contactSchema = z.object({
  name: z
    .string()
    .min(2, { message: "お名前は2文字以上で入力してください" })
    .max(50, { message: "お名前は50文字以内で入力してください" }),
  email: z
    .string()
    .email({ message: "有効なメールアドレスを入力してください" }),
  message: z
    .string()
    .min(10, { message: "お問い合わせ内容は10文字以上で入力してください" })
    .max(1000, { message: "お問い合わせ内容は1000文字以内で入力してください" }),
});

export type ContactFormValues = z.infer<typeof contactSchema>;

3. Action の作成

次に、フォーム送信を処理する Action を作成します:

// actions.ts
"use server";

import { parseWithZod } from "@conform-to/zod";
import { contactSchema } from "./schema";
import { revalidatePath } from "next/cache";

export async function submitContact(prevState: unknown, formData: FormData) {
  // フォームデータのバリデーション
  const submission = parseWithZod(formData, {
    schema: contactSchema,
  });

  // バリデーションエラーがある場合は早期リターン
  if (submission.status !== "success") {
    return submission.reply();
  }

  // バリデーション成功時のデータ
  const validData = submission.value;

  try {
    // ここでデータベースへの保存やメール送信などの処理を行う
    // 例: await saveToDatabase(validData);

    // キャッシュの再検証
    revalidatePath("/");

    // 成功時の処理
    return submission.reply({
      resetForm: true,
      success: "お問い合わせを受け付けました。ありがとうございます!",
    });
  } catch (error) {
    // エラー時の処理
    return submission.reply({
      error: "エラーが発生しました。再度お試しください。",
    });
  }
}

この Action では、以下の処理を行っています:

  • parseWithZod を使ってフォームデータをバリデーション
  • バリデーション成功時にデータ処理(実際のアプリでは DB への保存など)
  • revalidatePath を使ってページキャッシュを再検証
  • 結果に応じたレスポンスを返却

4. フォームコンポーネントの実装

最後に、Conform と Action を使ったフォームコンポーネントを作成します:

// ContactForm.tsx
"use client";

import { useForm } from "@conform-to/react";
import { parseWithZod } from "@conform-to/zod";
import { useActionState } from "react-dom";
import { contactSchema } from "./schema";
import { submitContact } from "./actions";

export function ContactForm() {
  // useActionState を使って Action を接続
  const [lastResult, action] = useActionState(submitContact, undefined);

  // useForm を使ってフォーム状態を管理
  const [form, fields] = useForm({
    // 最後の送信結果を同期
    lastResult,

    // クライアント側でもバリデーションを実行
    onValidate({ formData }) {
      return parseWithZod(formData, { schema: contactSchema });
    },

    // バリデーションのタイミング設定
    shouldValidate: "onBlur",
    shouldRevalidate: "onInput",
  });

  return (
    <form id={form.id} onSubmit={form.onSubmit} action={action} noValidate>
      {/* フォーム全体のエラー/成功メッセージ */}
      {lastResult?.success && (
        <div className="success">{lastResult.success}</div>
      )}
      {lastResult?.error && <div className="error">{lastResult.error}</div>}

      <div>
        <label htmlFor={fields.name.id}>お名前</label>
        <input
          id={fields.name.id}
          name={fields.name.name}
          defaultValue={fields.name.initialValue}
        />
        <div className="error">{fields.name.errors}</div>
      </div>

      <div>
        <label htmlFor={fields.email.id}>メールアドレス</label>
        <input
          id={fields.email.id}
          name={fields.email.name}
          type="email"
          defaultValue={fields.email.initialValue}
        />
        <div className="error">{fields.email.errors}</div>
      </div>

      <div>
        <label htmlFor={fields.message.id}>お問い合わせ内容</label>
        <textarea
          id={fields.message.id}
          name={fields.message.name}
          defaultValue={fields.message.initialValue}
          rows={5}
        />
        <div className="error">{fields.message.errors}</div>
      </div>

      <button type="submit">送信する</button>
    </form>
  );
}

このコンポーネントでは、以下の機能を実装しています:

  • useActionState フックで Action との連携
  • useForm フックでフォーム状態の管理
  • クライアント側でのバリデーション設定(onBluronInput時)
  • Server Action からの成功/エラーメッセージの表示
  • 各フィールド用の入力とエラー表示

これで、Conform と Server Actions を使った完全なフォーム実装が完成しました 👍

Conform が実現するプログレッシブエンハンスメントとは?

Conform の、重要な特徴の一つに「プログレッシブエンハンスメント」があります。

これは何を意味するのでしょうか?

https://developer.mozilla.org/en-US/docs/Glossary/Progressive_Enhancement

プログレッシブエンハンスメントとは、基本的な機能をすべてのブラウザで利用可能にしつつ、高度なブラウザではより優れた機能を提供する設計手法です

Conform のコンテキストでは、これは以下を意味します:

  1. JavaScript なしでも動作:JS が無効でもフォームは基本的な HTML フォームとして機能
  2. 拡張された体験:JS が有効な場合は、リアルタイムバリデーションやスムーズな UX を提供
  3. アクセシビリティ:基本的なフォーム機能がすべてのユーザーに提供される

Actions と組み合わせることで、この原則が完全に実装されます:

  • JS がない場合:フォームは通常の HTML フォーム送信として機能し、サーバーサイドでバリデーション
  • JS がある場合:クライアントサイドでのバリデーションとスムーズなフォーム体験を提供

これは、React Hook Form などのライブラリとは、異なるアプローチですね!

おわりに

最後まで読んでいただき、ありがとうございます 🥳

ハンズオン形式で、
実際に手を動かしてフォーム実装を学習したい場合は、以下の教材もチェックしてみてください!!

https://zenn.dev/kazzyfrog/books/handsonbook-nextjs-form

この記事が、少しでも参考になれば嬉しいです!

そして、もし、間違いや補足情報などがありましたら、ぜひコメントを追加してください!

Happy Hacking :)

参考

https://conform.guide/
https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations
https://github.com/edmundhung/conform
https://zenn.dev/bmth/articles/conform-to-complex
https://zenn.dev/akfm/articles/server-actions-with-conform
https://zenn.dev/cybozu_frontend/articles/think-about-pe

b13o Tech Blog

Discussion