ZodとConformを使ってRemixでバリデーションを実施する

RemixZodConformValidationSSRTypeScript

はじめに

個人で開発しているプロダクトにおいて、入力フォームを設置しています。 この入力フォームのバリデーションには、ZodConformを利用しています。 Remixでバリデーションを行う際には、サーバーサイドとクライアントサイド両方で実施することができます。 この実装方法について、記載していきます。

実装方法

入力バリデーション自体はConformを使いますが、その基本となるSchema定義にはZodを利用しています。 そのため、まずはZodによるSchema定義の方法をお伝えします。 その上で、Conformによるクライアントサイドならびにサーバーサイドそれぞれでのバリデーションの実装方法を記載します。

ZodによるSchema定義

今回の入力フォームはキーワード(keyword)とそれに対する説明(description)を入力するフォームとなります。 この2つの入力項目に対するSchemaは以下のように定義できます。

import { z } from 'zod'

export const WordSchema = z.object({
	keyword: z
		.string()
		.min(1, 'a new word is required.')
		.max(256, 'a new word is too long.'),
	description: z
		.string()
		.min(1, 'a description is required.')
		.max(2024, 'a description word is too long.')
})

それぞれの入力項目に対して、型と入力する文字列の長さの最小値と最大値を指定しています。 また、max(256, 'a new word is too long.')の第2引数ように、バリデーションに違反した際のメッセージも合わせて記載しています。

クライアントサイドのバリデーション

入力フォームとクライアントサイドのバリデーションに関わるコードのみ抜粋すると以下のようになります。

import { Form } from '@remix-run/react'
import { getFormProps, getInputProps, useForm } from '@conform-to/react'
import { parseWithZod } from '@conform-to/zod'
import { WordSchema } from '~/schema'
import FormError from '~/components/formError'

export default function Index() {
	...

	const [form, fields] = useForm({
		shouldValidate: 'onBlur',
		shouldRevalidate: 'onBlur',
		onValidate({ formData }) {
			return parseWithZod(formData, { schema: WordSchema })
		}
	})

	return (
		...

		<Form className="..." method="post" {...getFormProps(form)}>
			<label className="...">
				<div className="...">
					<p className="...">word</p>
					<p className="...">Required</p>
				</div>
				<input
					{...getInputProps(fields.keyword, { type: 'text' })}
					className="..."
					id="keyword"
					name="keyword"
					type="text"
					placeholder="e.g. Keyword"
				/>

				<FormError errors={fields.keyword.errors} />
			</label>

			// descriptionにおける入力フォームのUIも同様
		</Form>

		...
	)
}

まずは、useFrom()によってフォーム要素(form)や入力項目(fields)を取得しています。 shouldValidateshouldRevalidateは、入力においてどのタイミングでバリデーションを実施するかというものを指定しています。 また、onValidate()にて入力フォームの各値(formData)が呼び出されます。 この呼び出されるformDataと事前に定義したSchema(WordSchema)をparseWithZod(formData, { schema: WordSchema })のように渡すことで、バリデーションを実施することができます。

一方、UIコンポーネントについてです。 <From>タグにある{...getFromProps(form)}ですが、これはフォーム要素に対してアクセス可能にするために必要なpropsを取得するための関数になります。 また、{...getInputProps(fields.keyword, { type: 'text' })}は、インプット要素に対してアクセス可能にするために必要なpropsを取得するための関数となります。

サーバーサイドのバリデーション

サーバーサイドのバリデーションは以下のコードのように実装できます。

import { ActionFunction } from '@remix-run/cloudflare'
import { parseWithZod } from '@conform-to/zod'
import { WordSchema } from '~/schema'

export const action: ActionFunction = async ({ request }) => {
	const formData = await request.formData()
	const submission = parseWithZod(formData, { schema: WordSchema })

	if (submission.status !== 'success') {
		return submission.reply()
	}

	console.log(submission.value)
	return submission.value
}

export default function Index() {
	...
}

Remixでは、export const action: ActionFunction = async () => {}によって、サーバーサイドでの動きを規定することができます。 バリデーション自体は、クライアントサイドと同様に、parseWithZod()にて実現しています。 今回の例では、単純にバリデーションを行うだけとなっていますが、APIへのリクエストなども合わせて記載するのが実際のユースケースになると思います。

さいごに

いかがでしたでしょうか。自身の備忘のためのポストとなっておりますので、わかりにくい部分もあるかと思います。 誰かの役に立てれば、幸いです。