😵💫
React Hook Formのバリデーションが意図しないタイミングで走って詰まった話
概要
自分がReact Hook Form弱者過ぎるのを痛感するこの頃。
今回詰まったのはzodとReact Hook Form管理のフォームの要素に子コンポーネントを介して値を入れた際に全てのフィールドにバリデーションが走ってしまい、迷走しまっくった話です。
この記事が自分と同じようなケースにあたる、あたった人の役に立てば幸いです。
(そもそもの設計が悪いと言われればそれまでですが、、、)
結論
buttonタグにtype="button"をしてすることでこれを回避できる。
スタック
- Next.js
- React Hook Form
- zod
詳細
今回のケースを再現したコードを以下に示します。
まずは大元となるコンポーネントになります。
"use client";
import { useForm, SubmitHandler } from "react-hook-form";
import FormChild from "./FormChild";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
const exampleSchema = z.object({
exampleOneChar: z
.string()
.nonempty("必須項目です")
.max(10, "10文字以内で入力して下さい。"),
exampleThreeChars: z
.string()
.nonempty("必須項目です")
.max(3, "3文字以内です。"),
childExampleChars: z
.string()
.nonempty("必須項目です")
.max(5, "3文字以内です。"),
});
type exampleType = z.infer<typeof exampleSchema>; // exampleType
export default function PostForm() {
const {
register,
handleSubmit,
formState: { errors, isValid },
setValue,
} = useForm<exampleType>({
mode: "onChange",
resolver: zodResolver(exampleSchema),
});
const onSubmit: SubmitHandler<exampleType> = (data) => console.log(data);
return (
<div className="w-full max-w-xs items-center">
<form
className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4"
onSubmit={handleSubmit(onSubmit)}
>
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2">
OneChar
</label>
<input
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
type="text"
{...register("exampleOneChar")}
/>
<p className="text-red-500 text-xs italic">
{errors.exampleOneChar?.message}
</p>
</div>
<div className="mb-6">
<label className="block text-gray-700 text-sm font-bold mb-2">
exampleThreeChars
</label>
<input
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
type="text"
{...register("exampleThreeChars")}
/>
<p className="text-red-500 text-xs italic">
{errors.exampleThreeChars?.message}
</p>
</div>
<div className="mb-6">
<label className="block text-gray-700 text-sm font-bold mb-2">
childExampleChars
</label>
<input
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
type="text"
{...register("childExampleChars")}
/>
<p className="text-red-500 text-xs italic">
{errors.exampleThreeChars?.message}
</p>
</div>
{/* 今回詰まった要因となるこコンポーネント */}
<FormChild onApply={(text) => setValue("childExampleChars", text)} />
<div className="flex items-center justify-between">
<button
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
type="submit"
>
Submit
</button>
</div>
</form>
</div>
);
}
次に問題となる子コンポーネントを示します。
interface FormChildProps {
onApply: (text: string) => void;
}
const FormChild = (props: FormChildProps) => {
return (
<div className="grid grid-rows-2 gap-2 ">
<div>"Sample"というワードをフィールドに適応します。</div>
<div>
{/* ここが問題になるbuttonタグです */}
<button
onClick={() => {
props.onApply("Sample");
}}
className="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
>
Apply
</button>
</div>
</div>
);
};
export default FormChild;
上記のコードのコメントにもあるのですが、子コンポーネントのbuttonにtype指定がないことでApplyボタンをクリックするとsubmitされ、そのタイミングで全てのバリデーションが走っていたようです。
(自分はtype="submit"を入れない限りtype="button"として扱ってくれると思ってました。。。)
そのため、以下のように子コンポーネントのようにbuttonにtype="button" を入れることでバリデーションが全体に走ることを回避することができ、フィールド個別にバリデーションをかけることができます。
interface FormChildProps {
onApply: (text: string) => void;
}
const FormChild = (props: FormChildProps) => {
return (
<div className="grid grid-rows-2 gap-2 ">
<div>"Sample"というワードをフィールドに適応します。</div>
<div>
{/* ここが問題になるbuttonタグです */}
{/* type="button"を追加 */}
<button
type="button"
onClick={() => {
props.onApply("Sample");
}}
className="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
>
Apply
</button>
</div>
</div>
);
};
export default FormChild;
注意点
上記のようなケースの場合、フォームのバリデーションを走らせるパターンやアプリの設計に工夫が必要です。
今回はその辺の言及については避けます。
まとめ
子コンポーネントのボタンクリックを介して親コンポーネントのフォームフィールドに値を入れたい場合は子コンポーネントのbuttonにtype="button"を入れることで意図してない全体のバリデーションを避けることができる。
Discussion
調べてみるとこの挙動はReact Hook Formではなく、HTMLの機能由来のもので、ドキュメントによるとbuttonのtypeを指定してない場合は以下のような挙動となるらしい。
知らなかった...
他の記事でもそもそもbuttonのtypeは明示的に書いた方が良いと言われています。
これから気をつけていきたい。