フォームライブラリの conform を試す


Conform はプログレッシブエンハンスメントを意識して作られたフォームライブラリ。
その他の特徴は、type-safe なフィールド推論が可能なこと、a11y ヘルパーが用意されていること、そしてServer Actions に対応していること。
この辺りは、試しながら理解していけたらなと思う。


チュートリアルをやってみようか。
チュートリアルは Remix メインで書かれているし、自分好みに変更しつつ実装していこう。

インストール
npm install @conform-to/react @conform-to/zod --save

フォームは userName と email の2つのフィールドを持つこととする。

schema の定義。
import { z } from "zod";
export const schema = z.object({
userName: z
.string({ required_error: 'ユーザー名を入力してください'})
.min(1, { message: 'ユーザー名を入力してください' })
.min(2, { message: 'ユーザー名は2文字以上にしてください' })
.max(14, { message: 'ユーザー名は10字以内にしてください' }),
email: z.string({ required_error: 'メールアドレスを入力してください'}).email({ message: 'メールアドレスの形式で入力してください' }),
})

action の定義。
parseWithZod 関数を利用する。この関数を利用することで、フォームデータを zod のスキーマに従ってパースすることができる。エラーがある場合はエラーを返す。
'use server'
import { parseWithZod } from "@conform-to/zod"
import { schema } from "./schema"
import { redirect } from "next/navigation"
export const userRegister = async (formData: FormData) => {
const submission = parseWithZod(formData, { schema })
// パースに失敗した場合は、クライアントに報告
if (submission.status !== 'success') {
return submission.reply()
}
console.log(submission.value)
return redirect('/home')
}

あ、userFormState 利用するから以下のように修正する必要があるわ。エラー出てた。
prevState の型は一旦 any で。
'use server'
import { parseWithZod } from "@conform-to/zod"
import { schema } from "./schema"
import { redirect } from "next/navigation"
export const userRegister = async (prevState: any, formData: FormData) => {
const submission = parseWithZod(formData, { schema })
// パースに失敗した場合は、クライアントに報告
if (submission.status !== 'success') {
return submission.reply()
}
console.log(submission.value)
return redirect('/home')
}

フォームの作成。
フォームメタデータは conform の useForm フックで管理できるみたい。
'use client'
import { useFormState } from "react-dom"
import { userRegister } from "../actions"
import { useForm } from "@conform-to/react"
export const Form = () => {
const [lastResult, action] = useFormState(userRegister, undefined)
const [form, fields] = useForm({
lastResult,
// バリデーションのタイミング。入力時にバリデーションを行う
shouldValidate: 'onInput',
// リバリデーションのタイミング(送信ボタンを押した後)。入力時にリバリデーションを行う
shouldRevalidate: 'onInput',
})
console.log('field',fields.userName)
return (
<form action={action} id={form.id} className="m-4">
<div>
<label htmlFor={fields.userName.id}>Username</label>
<input
id={fields.userName.id}
type="text"
name={fields.userName.name}
aria-invalid={fields.userName.errors ? true : undefined}
aria-describedby={fields.userName.errors ? fields.userName.errorId : undefined}
className="border-2 border-gray-300 rounded-md p-2 w-full"
/>
<div id={fields.userName.errorId} className="text-red-500">{fields.userName.errors}</div>
</div>
<div>
<label htmlFor={fields.email.id}>Email</label>
<input
id={fields.email.id}
type="email"
name={fields.email.name}
aria-invalid={fields.email.errors ? true : undefined}
aria-describedby={
fields.email.errors ? fields.email.errorId : undefined
}
className="border-2 border-gray-300 rounded-md p-2 w-full"
/>
<div id={fields.email.errorId} className="text-red-500">{fields.email.errors}</div>
</div>
<button className="px-2.5 py-2 bg-blue-500 rounded-md text-white">送信</button>
</form>
)
}


現状、自力で属性を書いているが、getFormProps や getInputProps といったヘルパー関数を利用することで、アクセシビリティ関連の属性を自動で追加することができるみたい。
'use client'
import { useFormState } from "react-dom"
import { userRegister } from "../actions"
import { getFormProps, getInputProps, useForm } from "@conform-to/react"
export const Form = () => {
const [lastResult, action] = useFormState(userRegister, undefined)
const [form, fields] = useForm({
lastResult,
// バリデーションのタイミング。入力時にバリデーションを行う
shouldValidate: 'onInput',
// リバリデーションのタイミング(送信ボタンを押した後)。入力時にリバリデーションを行う
shouldRevalidate: 'onInput',
})
console.log('field',fields.userName)
return (
<form {...getFormProps(form)} action={action} className="m-4">
<div>
<label htmlFor={fields.userName.id}>Username</label>
<input
{...getInputProps(fields.userName, { type: 'text'})}
className="border-2 border-gray-300 rounded-md p-2 w-full"
/>
<div id={fields.userName.errorId} className="text-red-500">{fields.userName.errors}</div>
</div>
<div>
<label htmlFor={fields.email.id}>Email</label>
<input
{...getInputProps(fields.email, { type: 'email' })}
className="border-2 border-gray-300 rounded-md p-2 w-full"
/>
<div id={fields.email.errorId} className="text-red-500">{fields.email.errors}</div>
</div>
<button className="px-2.5 py-2 bg-blue-500 rounded-md text-white">送信</button>
</form>
)
}
しっかりアクセシビリティ関連の属性が追加されている。

現状入力フォームに文字を入力するたびにサーバー側にリクエストがいき、サーバー側でバリデーションが実行されている状態なのでよろしくない。また、サーバーでバリデーションを行うと多少の遅延があるという問題もある。
どうやら、クライアントでバリデーションを実行する方法もあるみたい。
onValidate を定義するという方法。
以下のように onValidate を定義することで、送信ボタンを押した場合のみサーバー側に送信することができる。
'use client'
import { useFormState } from "react-dom"
import { userRegister } from "../actions"
import { getFormProps, getInputProps, useForm } from "@conform-to/react"
import { parseWithZod } from "@conform-to/zod"
import { schema } from "../schema"
export const Form = () => {
const [lastResult, action] = useFormState(userRegister, undefined)
const [form, fields] = useForm({
lastResult,
onValidate({ formData }) {
return parseWithZod(formData, { schema: schema })
},
// バリデーションのタイミング。入力時にバリデーションを行う
shouldValidate: 'onInput',
// リバリデーションのタイミング(送信ボタンを押した後)。入力時にリバリデーションを行う
shouldRevalidate: 'onInput',
})
console.log('field',fields.userName)
return (
<form {...getFormProps(form)} action={action} onSubmit={form.onSubmit} className="m-4">
<div>
<label htmlFor={fields.userName.id}>Username</label>
<input
{...getInputProps(fields.userName, { type: 'text'})}
className="border-2 border-gray-300 rounded-md p-2 w-full"
defaultValue={'aaa'}
/>
<div id={fields.userName.errorId} className="text-red-500">{fields.userName.errors}</div>
</div>
<div>
<label htmlFor={fields.email.id}>Email</label>
<input
{...getInputProps(fields.email, { type: 'email' })}
className="border-2 border-gray-300 rounded-md p-2 w-full"
/>
<div id={fields.email.errorId} className="text-red-500">{fields.email.errors}</div>
</div>
<button className="px-2.5 py-2 bg-blue-500 rounded-md text-white">送信</button>
</form>
)
}

チュートリアルは以上でおしまい。
getFormProps と getInputProps を利用することで、 a11y に関連する属性を自動で設定してくれるのめちゃくちゃ良かった。