
こんにちは!!
BOXIL SaaSのエンジニア兼テックブログチームの平社員をしているブラーバです。最近は働きが認められ、テックブログチームで確固たる地位を築きつつあるとかないとか...。
今回は以前公開したReact Hook Form、Zod、Recoilを組み合わせたフォームを作る!にならい、React Hook FormとZodを使ったフロントエンド開発の第二弾です!!
本記事では、APIリクエストが必要なバリデーションをReact Hook FormとZodを使って実装しようとした際に、遭遇した問題とその解決策について話します。
同じような問題に直面している人、あるいはReact Hook FormやZodに自体に興味がある人の参考になると嬉しいです!!
遭遇してしまった問題
BOXIL SaaSでは一部フォームをReact Hook Formで作り、バリデーションにはZodを使用しています。
そのバリデーションの中には、ユーザーが入力した内容ががユニークかどうかを確認するために、バックエンドに問い合わせる項目がありました。
Zodの仕様では、いずれかの項目が入力されるたびに都度バリデーションが行われるため、問い合わせが必要な項目以外を入力していても、APIリクエストをしていました。React Hook Formのリポジトリでも同様の議論がされています。
https://github.com/orgs/react-hook-form/discussions/9005
以下のコードはisUniqueNameという関数で入力されたnameがユニークかどうかを確認するために、入力ごとにAPIリクエストを送信しているコードです...。
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
const isUniqueName = async (name: string) => {
console.log("isUniqueName: ", name);
// ここでAPIリクエストを飛ばし、DBに保存されている同一のnameがあるかを確認したい
return true;
};
const useUserForm = z.object({
id: z.string(),
name: z.string().refine(isUniqueName),
});
type UseUserForm = z.infer<typeof useUserForm>;
const defaultValues: UseUserForm = {
id: "",
name: "",
};
export function Hoge() {
const { register, handleSubmit } = useForm<UseUserForm>({
resolver: zodResolver(useUserForm),
mode: "onChange",
defaultValues,
});
return (
<>
<form onSubmit={handleSubmit((data) => console.log(data))}>
<input {...register("id")} />
<input {...register("name")} />
<button type="submit">submit</button>
</form>
</>
);
}
実際に上記のコードを動かしてみると、idなど他項目を入力していてもisUniqueNameが呼ばれてしまい、実際にconsole.logの部分をAPIリクエストに置き換えたとすると、計10回もAPIリクエストが飛んだことになります。

上記画像だと、本来ならAPIリクエストは最大でも4回に抑えたいです。
解決策
そこでブラウザのメモリ上にすでにAPIコールしたユーザー名を保持するという方法でAPIリクエストの回数を減らしました。
具体的には、APIに問い合わせをしたユーザー名をMapオブジェクトで管理し、再度同じ名前でバリデーションが走ったときにはAPIリクエストをせずにfalseを返すようにしました。
これでAPIを叩く回数を大幅に減らすことができました。
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
const existsName = new Map<string, boolean>();
const isUniqueName = async (name: string) => {
if (existsName.has(name)) {
return false;
}
console.log("isUniqueName: ", name);
existsName.set(name, true);
return true;
};
const useUserForm = z.object({
id: z.string(),
name: z.string().refine(isUniqueName),
});
type UseUserForm = z.infer<typeof useUserForm>;
const defaultValues: UseUserForm = {
id: "",
name: "",
};
export function Hoge() {
const { register, handleSubmit } = useForm<UseUserForm>({
resolver: zodResolver(useUserForm),
mode: "onChange",
defaultValues,
});
return (
<>
<form onSubmit={handleSubmit((data) => console.log(data))}>
<input {...register("id")} />
<input {...register("name")} />
<button type="submit">submit</button>
</form>
</>
);
}
実際に上記のコードを動かしてみると重複したログは出力されず、無駄なAPIリクエストが減っていました。

おわりに
以上、React Hook FormとZodを使って非同期バリデーションを最適化した話でした。
また、実際にBOXIL SaaSの開発時にこの問題に遭遇したときにはReact Hook FormでZodを使うときの5つパターンを参考に本記事のような実装をしました。
今回は、React Hook FormとZodを使ったフロントエンド開発の第二弾でしたが、第三弾も近いうちに公開する予定ですので、お楽しみに!!