convenience-store oden illust 1756

type-safe とプログレッシブエンハンスメント、アクセシビリティヘルパーを備えたフォームライブラリ Conform

Conform は React 向けのフォームライブラリです。type-safe であること、Web 標準を利用したプログレッシブエンハンスメントや、アクセシビリティヘルパーを特徴としており、Next.js の Server Actions や Remix に対応しています。

Conform は React 向けのフォームライブラリです。type-safe であること、Web 標準を利用したプログレッシブエンハンスメントや、アクセシビリティヘルパーを特徴としており、Next.js の Server Actions や Remix に対応しています。

Conform は以下の特徴を掲げています。

  • プログレッシブエンハンスメントファーストな API
  • Type-safe なフィールドの型推論
  • きめ細かいサブスクリプション
  • ビルドインのアクセシビリティヘルパー
  • Zod による型変換

Conform のチュートリアル

早速 Conform を使ってみましょう。Conform は Next.js と Remix の統合に対応しています。ここでは Next.js で Conform を使って簡単なフォームを作成するチュートリアルを紹介します。

インストール

以下のコマンドで Conform をインストールします。

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

スキーマの定義

始めに Zod を使ってフォームのスキーマを定義します。Conform は Zod と組み合わえることで型安全なフォームと、バリデーションを実現しています。

app/contact/schema.ts
import { z } from "zod";
 
export const schema = z.object({
  email: z.preprocess(
    (value) => (value === "" ? undefined : value),
    z.string({ required_error: "Email is required" }).email("Email is invalid")
  ),
  content: z.preprocess(
    (value) => (value === "" ? undefined : value),
    z
      .string({ required_error: "Content is required" })
      .min(10, "Content is too short")
      .max(1000, "Content is too long")
  ),
});

Server Action 関数の作成

次に、フォームの送信時に実行される Server Action 関数を作成します。parseWithZod 関数を使うことでフォームの値を Zod のスキーマに従ってパースを行います。parseWithZod 関数にはフォームの値とスキーマを渡すします。

app/contact/actions.ts
"use server";
import { parseWithZod } from "@conform-to/zod";
import { schema } from "./schema";
import { redirect } from "next/navigation";
 
export const contact = async (prevState: unknown, formData: FormData) => {
  const submission = parseWithZod(formData, { schema: schema });
 
  // フォームのバリデーションに失敗した場合
  if (submission.status !== "success") {
    return submission.reply();
  }
 
  // フォームの値を取り出す
  console.log("email:", submission.value.email);
  console.log("content:", submission.value.content);
 
  return redirect("/contact/success");
};

Zod によるバリデーションの結果は parseWithZod 関数によって返されるオブジェクトに含まれています。submission.status"success" であればバリデーションに成功したことを意味します。バリデーションに失敗した場合は submission.reply() メソッドを使ってエラーの情報とフォームに入力された値を返します。

フォームのフィールドの値は submittion.value から取り出せます。この値は Zod のスキーマに従って型が変換されています。

フォームの作成

最後にフォームを作成します。useForm フックを使うことでフォームの状態を管理するための API を提供しています。useForm フックを使う場合には "use client;" ディレクティブを宣言してクライアントコンポーネントとして扱う必要があります。

useFormState フックと組み合わせることで、前回入力したフォームの値を初期値としてフォームに表示できます。

app/contact/Form.tsx
"use client";
 
import {
  getInputProps,
  getTextareaProps,
  useForm,
} from "@conform-to/react";
import { useFormState } from "react-dom";
import { contact } from "./actions";
 
export function Form() {
  const [lastResult, action] = useFormState(contact, undefined);
  const [form, fields] = useForm({
    lastResult,
  });
 
  return (
    <form id={form.id} action={action} noValidate>
      <div>
        <label htmlFor={fields.email.id}>Email</label>
        <input {...getInputProps(fields.email, { type: "email" })} />
        <div id={fields.email.errorId}>{fields.email.errors}</div>
      </div>
      <div>
        <label htmlFor={fields.content.id}>Content</label>
        <textarea {...getTextareaProps(fields.content)}></textarea>
        <div id={fields.content.errorId}>{fields.content.errors}</div>
      </div>
      <button type="submit">Send</button>
    </form>
  );
}

getInputPropsgetTextareaProps といったヘルパー関数を使うことで、アクセシビリティ上必要な属性をフォームのフィールドに自動で追加できます。Email フィールドに getInputProps() を渡した結果は以下のようになります。

<div>
  <label for=":Rauukq:-email">Email</label>
  <input
    // ラベルに渡した fields.email.id と一致する
    id=":Rauukq:-email"
    // コントロールを form 要素に関連付ける <form> に渡した form.id と一致する
    form=":Rauukq:"
    type="email"
    // zod の schema のキー名に基づいた name 属性
    name="email"
    // エラーがある場合には aria-invalid 属性を true にする
    aria-invalid="true"
    // エラーがある場合には aria-describedby 属性にエラーメッセージの id を渡して関連付ける
    aria-describedby=":Rauukq:-email-error"
  />
  <div id=":Rauukq:-email-error">Email is invalid</div>
</div>

aria-invalidaria-describedby などのアクセシブルなフォームを実現するために複雑な設定の管理を隠蔽してくれるので、誰でも一定の水準を満たすフォームを作成できる点は魅力的です。

これで Conform を使ったフォームの作成が完了しました。フォームをサブミットした後にエラーメッセージが表示されることを確認してみましょう。また、プログレッシブエンハンスメントにより JavaScript が無効な環境においてもフォームが機能することを確認できます。

バリデーションのタイミングをコントロールする

デフォルトでバリデーションはフォームがサブミットされた後にサーバーサイドで実行されます。フォームからフォーカスが外れたタイミングなど、より早いタイミングでバリデーションを実行したい場合もあるでしょう。その場合には useForm フックの shouldValidateshouldRevalidate オプションを使うことでバリデーションのタイミングをコントロールできます。

app/contact/Form.tsx
export function Form() {
  const [lastResult, action] = useFormState(contact, undefined);
  const [form, fields] = useForm({
    lastResult,
    // ユーザーのフォーカスが離れたいタイミングで初めてバリデーションを実行する
    shouldValidate: "onBlur",
    // ユーザーの入力が変更されたタイミングでバリデーションを再実行する
    shouldRevalidate: "onInput",
  });

なお、バリデーションの実行タイミングを onSubmit 以外のタイミングに設定した場合でも、onSubmit でフォームをサブミットした際にはバリデーションが実行されるためプログレッシブエンハンスメントが損なわれることはありません。

shouldValidateshouldRevalidateサーバーサイドでバリデーションが実行されます。つまり、onInput でバリデーションが実行される場合には、ユーザーがタイプするたびにサーバーサイドにリクエストを送信して結果を待つことになります。Devtools のネットワークタブを見ると、フォームの入力が変更されるたびにリクエストが送信されていることが確認できます。

クライアントでバリデーションを実行する

メールアドレスの重複チェックのように、データベースに問い合わせる必要があるバリデーションであればサーバー側でチェックすることは理にかなっています。しかし、今回の例のように簡単なバリデーションであればクライアントサイドでバリデーションを実行することで、より早いフィードバックを返すことができます。また、React + Vite のようにサーバーを持たない SPA として開発している場合には、サーバーサイドでバリデーションを実行することができないため、クライアントでバリデーションを実行する必要があります。

クライアントでバリデーションを実行させるためには、useForm フックが返す form.onSubmit ハンドラーを <form> にわたす必要があります。また、クライアントのバリデーションは useForm のオプションである onValidate メソッド内で実行されます。このメソッド内で、サーバーサイドの処理と同じように parseWithZod 関数を使ってバリデーションを実行します。

app/contact/Form.tsx
export function Form() {
  const [lastResult, action] = useFormState(contact, undefined);
  const [form, fields] = useForm({
    lastResult,
    shouldValidate: "onBlur",
    shouldRevalidate: "onInput",
    onValidate({ formData }) {
      return parseWithZod(formData, { schema: schema });
    },
  });
 
  return (
    <form
      id={form.id}
      onSubmit={form.onSubmit}
      action={action}
      noValidate
    >

obSubmit ハンドラーを使用している場合には、クライアントでバリデーションが満たされない限りフォームがサブミットされることはありまえん。

まとめ

  • Next.js や Remix のような Web 標準に従って構築されたフォームを使うフレームワークと組み合わせて使用することを前提として作られており、プログレッシブエンハンスメントやアクセシビリティの観点で優れたフォームライブラリ
  • getInputPropsgetTextareaProps といったヘルパー関数を使うことで、アクセシビリティ上必要な属性をフォームのフィールドに自動で追加できる
  • Zod と組み合わせることで、型安全なフォームとバリデーションを実現している
  • サーバーサイドでのバリデーションの実行タイミングをコントロールすることができるため、より柔軟なフォームの作成が可能

参考


Contributors

> GitHub で修正を提案する
この記事をシェアする
はてなブックマークに追加

関連記事