React Hook FormとZodを組み合わせて利用する
こんにちは。食べログ フロントエンドチームの原田です。
現在担当しているプロジェクトで、React Hook FormとZodを組み合わせて利用する機会があったので、今回はReact Hook Formの基本的な使い方からスキーマバリデーションをZodに任せる方法を紹介をしたいと思います。
React Hook Formとは
React Hook Form は「高性能で柔軟かつ拡張可能な使いやすいフォームバリデーションライブラリ」をテーマに掲げた入力フォームの管理に特化した React 向けのライブラリです。
なぜReact Hook Formを利用したか
今回のプロジェクトでは複雑なフォームを組む必要があり、フォームの状態管理を任せられる点や、パフォーマンス面、ドキュメントや検索でヒットする情報の多さからReact Hook Formを利用することを決めました。
基本的な使い方
まずはReact Hook Formの基本的な使い方を以下のサンプルコードを元に簡単に説明します。
import { FC } from "react";
import { useForm, SubmitHandler } from "react-hook-form";
type Inputs = {
firstName: string;
lastName: string;
email: string;
age: number;
};
export const Form: FC = () => {
const { register, handleSubmit, formState: { errors } } = useForm<Inputs>();
const onSubmit: SubmitHandler<Inputs> = data => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("firstName", { maxLength: 50 })} />
{errors.firstName && <p>50文字以内で入力してください</p>}
<input {...register("lastName", { maxLength: 50 })} />
{errors.lastName && <p>50文字以内で入力してください</p>}
<input {...register("email", { required: true })} />
{errors.email && <p>email is required</p>}
<input type="number" {...register('age'), { min: 10 }} />
{errors.age && <p>10以上で入力してください</p>}
<input type="submit" />
</form>
);
}
フィールドの登録
React Hook Formからはフォームを管理するためのカスタムフックであるuseFormが提供されています。
まずは管理対象となる入力フォームをフックに登録します。
useFormから提供されているregisterを利用します。
registerにはkey としてユニークな name 属性を指定する必要があります。
keyを指定したregisterの戻り値をスプレッド構文で管理対象となるエレメントに渡します。
...
const { register, handleSubmit, formState: { errors } } = useForm<Inputs>();
...
<input {...register("firstName")} />
...
Submit時の処理
useFormから提供されているhandleSubmitは、フォームの入力内容を検証したうえで引数に渡したハンドラーを実行します。
ハンドラーで受け取れるdataには検証された入力値が入ってきます。
...
const { register, handleSubmit, formState: { errors } } = useForm<Inputs>();
const onSubmit: SubmitHandler<Inputs> = data => console.log(data);
...
<form onSubmit={handleSubmit(onSubmit)}>
...
フォームのバリデーションを実装する & エラーを受け取る
registerにルールを設定することでバリデーションを実装することができます。
またuseFormから提供されているerrorsを監視することでフォームの入力にエラーがあったかどうかを判断することができます。
...
const { register, handleSubmit, formState: { errors } } = useForm<Inputs>();
...
<input {...register("firstName", { maxLength: 50 })} />
{errors.firstName && <p>50文字以内で入力してください</p>}
<input {...register("lastName", { maxLength: 50 })} />
{errors.lastName && <p>50文字以内で入力してください</p>}
<input {...register("email", { required: true })} />
{errors.email && <p>email is required</p>}
<input type="number" {...register('age'), { min: 10 }} />
{errors.age && <p>10以上で入力してください</p>}
...
Zodを使う
ここまでReact Hook Formの基本的な使い方を説明しましたが、
今回の実装ではバリデーションにZodを利用しました。
Zodとは
Zodとは、TypeScript First なスキーマバリデーションライブラリで、Blitzにも使われているライブラリです。
なぜZodを利用したか
registerにバリデーションを定義する場合、Presenter層にバリデーションの定義が散らばってしまいますが、
Zodを利用した場合はバリデーションの宣言を一箇所にまとめられるためです。
また公式ではスキーマバリデーションを行うライブラリとしてyupを紹介していますが、useFormに渡したい型が、Zodの場合はスキーマ定義からzod.inferで簡単に生成できたのに対して、
yupの場合は、yup.IntferTypeで生成した型が意図した型にならず、スキーマ定義とは別に自前の定義を行う必要がありそうでした。
その場合、スキーマ定義と自前の型定義が乖離しないように開発を進めていく必要がありそうだったため、今回はZodを採用しました。
Zodを組み合わせた例
Zodを利用したサンプルコードは以下になります。
import { FC } from "react";
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { schema, Schema } from "./validations/schema";
export const Form: FC = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<Schema>({
resolver: zodResolver(schema),
});
const onSubmit: SubmitHandler<Schema> = data => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('firstName')} />
{errors.firstName?.message && <p>{errors.firstName?.message}</p>}
<input {...register('lastName')} />
{errors.lastName?.message && <p>{errors.lastName?.message}</p>}
<input {...register('email')} />
{errors.email?.message && <p>{errors.email?.message}</p>}
<input type="number" {...register('age')} />
{errors.age?.message && <p>{errors.age?.message}</p>}
<input type="submit" />
</form>
);
};
import { z } from "zod";
export const schema = z.object({
firstName: z.string().max(50),
lastName: z.string().max(50),
email: z.string().min(1, { message: 'Required' }),
age: z.number().min(10),
});
export type Schema = z.infer<typeof schema>;
スキーマバリデーションの定義を別ファイルに定義して、
Presenter層にバリデーションの定義を書かなくて良くなりました。
またuseFormに渡す型の生成もzod.inferを行うだけとなっています。
まとめ
ここまでReact Hook FormとZodを組み合わせて使う方法の基本的な部分を紹介しました。
React Hook Formにフォームの状態管理や、パフォーマンス面の考慮を任せることでピュアにフォームを開発する場合よりも開発スピードを上げることができます。
またZodを利用することでスキーマバリデーションの定義を外出ししてコードの見通しを良くすることができます。
記事の中でZodを採用した理由を紹介しましたが、Zodはyupより後発のライブラリで、yupよりも検索で見つかる情報が少なく若干迷いながら開発を進めなければならない場面がありました。
ただ慣れてくれば型推論や厳密性の面でメリットが大きいライブラリだと感じたので、今後もこのブログから情報を発信していければと思います。
最後に
現在、食べログではフロントエンドに関わるポジションとして以下の2つを募集しています。
気になった方は是非チェックしてみてください!
・フロントエンド統括チームに所属するフロントエンドエンジニア
・フロントエンドをメインにサービス開発を担当していくWEBエンジニア
どれかに当てはまった方は以下のリンクも是非御覧ください!