SMARTCAMP Engineer Blog

スマートキャンプ株式会社(SMARTCAMP Co., Ltd.)のエンジニアブログです。業務で取り入れた新しい技術や試行錯誤を知見として共有していきます。

React Hook Form と Zod で非同期バリデーションがしたいの!!

こんにちは!!

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リクエストが飛んだことになります。

改修前のconsole.log

上記画像だと、本来なら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リクエストが減っていました。

改修後のconsole.log

おわりに

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