React Hook Form × Zod × TypeScriptでフォームを作成する①
背景
フォームを作成することになりました。プロジェクト全体でフォームが沢山登場しそうな雰囲気だったので、よりフォームを簡単に扱うためのライブラリを入れることにしました。
この機会にキャッチアップ・整理したいと思います。
React Hook Formは、フォームのデータのバリデーションやエラー処理を簡単に行うことができ、より高速でパフォーマンスが高いことが特徴らしいです。
またZodを組み合わせることで、より複雑なバリデーションにも対応できそうだったので選択しました。
そもそもライブラリを使用しない場合
useState・useRef・そもそもHookを使用しない、など色々な手段がありました。
useStateを使用すると、下記のようになります。
import { useState } from "react"; export const App = () => { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); console.log({ email, password, }); }; return ( <div className="App"> <h1>ログイン</h1> <form onSubmit={handleSubmit}> <div> <label htmlFor="email">Email</label> <input id="email" name="email" value={email} onChange={e => { setEmail(e.target.value); }} /> </div> <div> <label htmlFor="password">Password</label> <input id="password" name="password" value={password} onChange={e => { setPassword(e.target.value); }} type="password" /> </div> <div> <button type="submit">ログイン</button> </div> </form> </div> ); }
シンプルなフォームを作成するのであれば、これで良い気もします。。
React Hook Formの使用
次にReact Hook Formのみを使用してフォームを作成していきます。
先ほどのuseStateを使用したフォームを作り替えてみます。
import { useForm, type SubmitHandler } from "react-hook-form"; type FormValues = { email: string; password: string; }; export const App = () => { const { register, handleSubmit } = useForm<FormValues>(); const onSubmit: SubmitHandler<FormValues> = data => { console.log(data); }; return ( <div className="App"> <h1>ログイン</h1> <form onSubmit={onSubmit}> <div> <label htmlFor="email">Email</label> <input id="email" {...register("email")} /> </div> <div> <label htmlFor="password">Password</label> <input id="password" {...register("password")} type="password" /> </div> <button type="submit">ログイン</button> </form> </div> ); }
register内部でrefが使われ、非常にシンプルな記述になりました。
参照:
useForm - register
React Hook Formのバリデーション
シンプルなバリデーション
次にReact Hook Formでバリデーションを作成します。
registerの箇所を{...register("email", { required: "入力が必須の項目です" })}とし、バリデーションを追加。
また、useFormからformState: { errors },を取得し、エラー情報を参照できるようにします。
return ( <div className="App"> <h1>ログイン</h1> <form onSubmit={onSubmit}> <div> <label htmlFor="email">Email</label> <input id="email" // 入力必須のバリデーションを追加 {...register("email", { required: "入力が必須の項目です" })} /> {/* エラーメッセージの表示 */} {errors.email?.message && <div>{errors.email.message}</div>} </div> <div> <label htmlFor="password">Password</label> <input id="password" {...register("password")} type="password" /> </div> <button type="submit">ログイン</button> </form> </div> );
複数のバリデーション
また、1つの入力項目に複数のバリデーションを作成することも可能。
デフォルトだと1つのエラー内容しか取れないので、useFormに{ criteriaMode: "all" }を渡し、
registerにもオブジェクト記法で複数のバリデーションを渡す。
const { register, handleSubmit, formState: { errors }, } = useForm<FormValues>({ criteriaMode: "all", });
デフォルト値
フォームにデフォルト値を設定することも可能。
useFormにdefaultValuesを渡す。
const { register, handleSubmit } = useForm<FormValues>({ defaultValues: { email: "test@test.com", password: "pass" }, });
バリデーションの発火タイミング
また、バリデーションのタイミングを変更することもできる。
デフォルトでは1回目はSubmit時で、それ以降はonChangeのタイミングでバリデーションを行う。
例えばuseFormに{ mode: "onChange" }を渡すと、最初からonChageのタイミングでバリデーションが行われるようになる。
const { register, handleSubmit, formState: { errors }, } = useForm<FormValues>({ mode: "onChange", });
入力状態のチェック
さらに、isDirtyを使用することで、フォームが未入力状態かどうか判定できる。
formState オブジェクトのプロパティにisDirtyを渡す。
submitボタンのdisabledの値として渡すことで、未入力時はボタンを無効にする挙動を実装することができる。
const { register, handleSubmit, formState: { isDirty }, } = useForm<FormValues>();
<button type="submit" disabled={!isDirty} > ログイン </button>
詳細な入力チェック
dirtyFieldsを使用すると、それぞれの入力項目に対して未入力状態かを判定できる。
const { register, handleSubmit, formState: { dirtyFields }, } = useForm<FormValues>();
<button type="submit" disabled={!(dirtyFields.email && dirtyFields.password)} > ログイン </button>
Zodの利用
ライブラリとリゾルバーをインストール。
% npm install @hookform/resolvers zod
スキーマを利用して、バリデーションの設定を行う。
import { zodResolver } from "@hookform/resolvers/zod"; import * as z from 'zod'; const schema = z.object({ email: z.string().email().min(1), password: z.string().min(1), });
また、定義した schema を利用するために useForm Hook の引数で zodResolver を利用。
const { register, handleSubmit, formState: { errors }, } = useForm({ resolver: zodResolver(schema), });
全体のコードとしては下記のようになりました。
/* eslint-disable @typescript-eslint/no-misused-promises */ import { Input } from "@chakra-ui/react"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm, type SubmitHandler } from "react-hook-form"; import * as z from "zod"; const schema = z.object({ email: z.string().email({ message: "メールアドレスの形式ではありません。" }), password: z.string().min(1, { message: "1文字以上入力する必要があります。" }), }); type FormValues = { email: string; password: string; }; const App = () => { const { register, handleSubmit, formState: { errors }, } = useForm<FormValues>({ resolver: zodResolver(schema) }); const onSubmit: SubmitHandler<FormValues> = data => { console.log(data); }; return ( <div className="App"> <h1>ログイン</h1> <form onSubmit={handleSubmit(onSubmit)}> <div> <label htmlFor="email">Email</label> <Input id="email" {...register("email")} /> <p>{errors.email?.message}</p> </div> <div> <label htmlFor="password">Password</label> <Input id="password" {...register("password")} type="password" /> <p>{errors.password?.message}</p> </div> <button type="submit">ログイン</button> </form> </div> ); }; export default App;
課題・残タスク
今後の課題として、制御系と非制御系でのReact Hook Formの使用方法の違いと、いまいちZodの利点を感じられなかったので、Zodを導入する利点について深堀してみたいと思いました。