react-hook-formハマりポイント① 🖍️zod:相関判定の発火タイミング
はじめに
react-hook-form
を使っていて、
いくつかハマりポイントがあったので記事にまとめようと思います。
react-hook-form
は有名なので説明不要かもしれませんが、公式を引用すると、
「パフォーマンス、柔軟性、拡張性に優れたフォームと、使いやすいバリデーション」
を提供してくれるライブラリです。
zod
はというと、TypeScriptファーストのスキーマ宣言・検証ライブラリであり、
react-hook-form
との相性もよく、カスタマイズ性も高いのが特徴です。
zodの使用例:ログインフォーム
バリデーションを定義したschema
をそのままFormData
型として使うことができます。
import { zodResolver } from '@hookform/resolvers/zod';
import { FC } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
const schema = z.object({
email: z.string().email({
message: 'メールアドレスの形式で入力してください'
}),
password: z
.string()
.regex(/^(?=.*?[a-z])(?=.*?\d)(?=.*?[A-Z])[!-~]{8,20}$/, {
message: '8文字以上20文字以下の英数字・記号を入力してください'
})
});
type FormData = z.infer<typeof schema>;
export const Login: FC = () => {
const {
register,
handleSubmit,
formState: { errors }
} = useForm<FormData>({
resolver: zodResolver(schema),
mode: 'onChange'
});
return (
<form onSubmit={handleSubmit(data => console.log(data))}>
<p>メールアドレス</p>
<input type="email" {...register("email")} />
{errors.email?.message && <p>{errors.email?.message}</p>}
<p>パスワード</p>
<input type="password" {...register("password")} />
{errors.password?.message && <p>{errors.password?.message}</p>}
<button>ログイン</button>
</form>
);
};
ハマった点
タイトルにもありますが、相関判定での各入力欄の判定タイミングで躓いた点について説明します。
例えば、以下のフォームを考えます。
- 「開始日」、「終了日」の入力欄を作成
- バリデーションの条件は、「開始日」が「終了日」以前の日付となること
- バリデーションの判定タイミングは
onChange
(useForm
にてmode: 'onChange’
を設定)
2つの入力欄の相関判定をしたいので、zod
のrefine
を使用して実装してみます。
refineとは
refine
は相関判定以外でも独自のバリデーションを記述することができます。
refine
の引数の1つ目は検証ロジック(期待される合格条件)、
2つ目はRefineParams
型のオプションで、エラーメッセージやエラーパスの指定などができます。
const schema = z
.object({
dateFrom: z.optional(z.string()),
dateTo: z.optional(z.string()),
})
.refine(
(value) =>
value.dateFrom === '' ||
value.dateTo === '' ||
dayjs(value.dateFrom) < dayjs(value.dateTo),
{
message: '開始日が終了日以前の日付となる様に設定してください',
path: ['dateFrom']
}
);
コード全体
const schema = z
.object({
dateFrom: z.optional(z.string()),
dateTo: z.optional(z.string()),
})
.refine(
(value) =>
value.dateFrom === '' ||
value.dateTo === '' ||
dayjs(value.dateFrom) < dayjs(value.dateTo),
{
message: '開始日が終了日以前の日付となる様に設定してください',
path: ['dateFrom']
}
);
type FormData = z.infer<typeof schema>;
export const DateSearchForm: FC = () => {
const {
register,
handleSubmit,
formState: { errors }
} = useForm<FormData>({
resolver: zodResolver(schema),
mode: 'onChange'
});
return (
<form
onSubmit={handleSubmit((data) => console.log(data))}
className="flex flex-col gap-2 justify-center w-96 p-10"
>
<p>開始日</p>
<input type="date" {...register('dateFrom')} />
<p>終了日</p>
<input type="date" {...register('dateTo')} />
{errors.dateFrom?.message != null && (
<p>{errors.dateFrom?.message}</p>
)}
<span>
<button className="border p-2">検索</button>
</span>
</form>
);
};
しかし、これだと指定したパスのdateFrom
(開始日)の方しかonChange
が適用されません。
dateTo
(終了日)の方で、条件に合格する様に日付を修正してもエラー文言は消えません。
(onSubmit
のタイミングでの判定は問題ありません。)
dataFrom
とdataTo
は主従関係のないデータだし、判定タイミングは揃えたい・・・!
どちらもonChange
で判定できるようにしたい・・・!!
やってみたこと
判定タイミングを追加
useForm
のtrigger
を使って、dateTo
の値を変更したタイミングでdateFrom
に紐づけられたrefine
の判定を実施すれば解決できそうです。
triggerとは
フォームや入力のバリデーションを手動でトリガします。
このメソッドは、依存バリデーション (入力バリデーションが別の入力値に依存する) を行う場合にも有用です。
<p>終了日</p>
<input
type="date"
{...register('dateTo')}
onChange={() => trigger('dateFrom')}
/>
しかし、この実装だとdateTo
の変更タイミングでエラー文は消えませんでした。
register
で値を取得するよりonChange
の方が早く動いているようです。
ログ検証
解決方法
registerのpropsのonChangeで判定する
useForm
のregister
のpropsにもonChange
がありました。
こちらを使って実装してみます。
<p>終了日</p>
<input
type="date"
{...register('dateTo', {
onChange: () => trigger('dateFrom')
})}
/>
できた✨
dateFrom
(開始日)・dateTo
(終了日)、どちらの変更時も正しい判定が反映されています✨
実は最初、「register
での値取得をやめてonChange
にてsetValue
とtrigger
を実施する」
という方法をやっていました。
それでも実現はできますが、違和感があったので、上記の方法に気付けてよかったです。
さいごに
他のライブラリと組み合わせて使う事も多いreact-hook-form
ですが、
今回はzod
と組み合わせで苦労した点のうちのひとつでした。
自分なりの解決方法なので、もっとスマートな方法があるかもしれません。
(こうすればよいよっていうのがあれば是非ご教授お願いします)
そして、他にも自分的なハマりポイントがあったので、またご紹介できたらと思います。
それでは読んでいただきありがとうございました。
Discussion