こんにちは!!
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を使ったフロントエンド開発の第二弾でしたが、第三弾も近いうちに公開する予定ですので、お楽しみに!!