🐣

react-hook-formハマりポイント① 🖍️zod:相関判定の発火タイミング

2023/07/13に公開

はじめに

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>
  );
};

https://zod.dev/

ハマった点

タイトルにもありますが、相関判定での各入力欄の判定タイミングで躓いた点について説明します。

例えば、以下のフォームを考えます。

  • 「開始日」、「終了日」の入力欄を作成
  • バリデーションの条件は、「開始日」が「終了日」以前の日付となること
  • バリデーションの判定タイミングはonChangeuseFormにてmode: 'onChange’を設定)

2つの入力欄の相関判定をしたいので、zodrefineを使用して実装してみます。

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のタイミングでの判定は問題ありません。)

dataFromdataToは主従関係のないデータだし、判定タイミングは揃えたい・・・!
どちらもonChangeで判定できるようにしたい・・・!!

Image from Gyazo

やってみたこと

判定タイミングを追加

useFormtriggerを使って、dateToの値を変更したタイミングでdateFromに紐づけられたrefineの判定を実施すれば解決できそうです。

triggerとは

フォームや入力のバリデーションを手動でトリガします。
このメソッドは、依存バリデーション (入力バリデーションが別の入力値に依存する) を行う場合にも有用です。

<p>終了日</p>
<input
    type="date"
    {...register('dateTo')}
    onChange={() => trigger('dateFrom')}
/>

しかし、この実装だとdateToの変更タイミングでエラー文は消えませんでした。
registerで値を取得するよりonChangeの方が早く動いているようです。

ログ検証

getValuesでログを出力。
onChange時には値がまだ入っていない様です。
Image from Gyazo

もう一度dateToを変更すると・・・
変更前の値「2023/6/26」が反映されており、また「2023/6/26」に対してtriggerが働いて判定されているのがわかります。
Image from Gyazo

解決方法

registerのpropsのonChangeで判定する

useFormregisterのpropsにもonChangeがありました。
こちらを使って実装してみます。

<p>終了日</p>
<input
    type="date"
    {...register('dateTo', {
        onChange: () => trigger('dateFrom')
    })}
/>

Image from Gyazo
できた✨
dateFrom(開始日)・dateTo(終了日)、どちらの変更時も正しい判定が反映されています✨

実は最初、「registerでの値取得をやめてonChangeにてsetValuetriggerを実施する」
という方法をやっていました。
それでも実現はできますが、違和感があったので、上記の方法に気付けてよかったです。

https://react-hook-form.com/docs/useform/register

さいごに

他のライブラリと組み合わせて使う事も多いreact-hook-formですが、
今回はzodと組み合わせで苦労した点のうちのひとつでした。
自分なりの解決方法なので、もっとスマートな方法があるかもしれません。
(こうすればよいよっていうのがあれば是非ご教授お願いします)
そして、他にも自分的なハマりポイントがあったので、またご紹介できたらと思います。

それでは読んでいただきありがとうございました。

エックスポイントワン技術ブログ

Discussion