🔄

React Hook Formのwatch()は多用しないほうがいい

に公開

結論

フォーム全体の値を取得したいとき以外、再レンダリングを最小化してパフォーマンスを保ちたいなら useWatch() を使う。

➡ 特定の値だけ監視したいなら useWatch()、全体をざっくり見たいなら watch() が便利!


🔁 使いどころまとめ

状況・目的 適した関数 理由
フォーム全体の値を一括で監視したい watch() 全体状態を簡単に取得できる
デバッグ用に値をログ出力したい watch() 全体の状態を都度確認しやすい
小規模なフォームでパフォーマンスが問題にならない watch() パフォーマンス負荷が少ないため
特定フィールドの変更だけを検知したい useWatch() 必要なフィールドだけ購読できる
大規模フォームやパフォーマンス重視 useWatch() 全体再レンダリングを防げる
一部の UI だけ動的に更新したい useWatch() 局所的な再レンダリングが可能

watch()とuseWatch()の違い

React Hook Form公式ドキュメントに以下のような記述がありました。

The only difference between useWatch and watch is at the root (useForm) level or the custom hook level update.

「useWatch と watch の唯一の違いは、useForm レベルまたはカスタムフックレベルでの更新にあります。」と書かれています。

つまり、簡単に言えば以下のような違いがあります。

  • watch() は「フォーム全体を包括的に監視」する(useFormレベル)。
  • useWatch() は「特定のフィールドだけを監視」する(カスタムフックレベル)。

useFormレベルとは、useForm() を定義したコンポーネントとその子コンポーネント全体
以下のコード例でいうと、MyFormコンポーネントとその子コンポーネント全体です。

import { useForm } from 'react-hook-form';

export default function MyForm() {
  const { register, watch } = useForm();

  return (
    <form>
      <InputA register={register} />
      <InputB register={register} />
    </form>
  );
}

次にwatch()とuseWatch()の違いをコードで見ていきます。

watch()の場合

"use client";
import { useFormContext, useWatch } from "react-hook-form";

// watch()を使ったコンポーネント
function NameDisplayWatch() {
  console.log("🔁 NameDisplayWatch が再レンダリングされました!");
  const { watch } = useFormContext();
  const name = watch("name");
  console.log(name);

  return <p>🔁 watch() の値: {name}</p>;
}

export default function Home() {
  console.log("🏠 Home コンポーネントが再レンダリングされました!");

  const { register } = useFormContext();

  return (
    <div className="p-10">
      <form className="flex flex-col gap-6">
        <div className="flex flex-col">
          <label htmlFor="name">ユーザー名</label>
          <input
            id="name"
            {...register("name")}
            type="text"
            className="border rounded-2xl"
          />
        </div>
      </form>

      <div className="mt-10 border-t pt-5 space-y-2">
        <NameDisplayWatch />
      </div>
    </div>
  );
}

watch()の場合、「あ」と入力すると...


「Home コンポーネントが再レンダリングされました!」と「NameDisplayWatch が再レンダリングされました!」が表示されます。
つまりNameDisplayWatchコンポーネントだけでなく、Homeコンポーネントもレンダリングされていることになります。

useWatch()の場合

"use client";
import { useFormContext, useWatch } from "react-hook-form";

// useWatch()を使ったコンポーネント
function NameDisplayUseWatch() {
  console.log("✅ NameDisplayUseWatch が再レンダリングされました!");
  const { control } = useFormContext();
  const name = useWatch({ control, name: "name" });
  return <p>useWatch() の値: {name}</p>;
}

export default function Home() {
  console.log("🏠 Home コンポーネントが再レンダリングされました!");

  const { register } = useFormContext();

  return (
    <div className="p-10">
      <form className="flex flex-col gap-6">
        <div className="flex flex-col">
          <label htmlFor="name">ユーザー名</label>
          <input
            id="name"
            {...register("name")}
            type="text"
            className="border rounded-2xl"
          />
        </div>
      </form>

      <div className="mt-10 border-t pt-5 space-y-2">
        <NameDisplayUseWatch />
      </div>
    </div>
  );
}


先ほどと同じように「あ」と入力すると...


今度は「NameDisplayUseWatch が再レンダリングされました!」のみが表示されており、Home コンポーネントは再レンダリングされていません。
つまり、今回はNameDisplayUseWatchコンポーネントのみがレンダリングされています。

レンダリング構造まとめ(イメージ)

watch()とuseWatch()のレンダリングの違いをまとめると以下のようになります。

watch() の場合:
Home 🔁(再レンダリング)
└─ NameDisplayWatch 🔁(再レンダリング)

useWatch() の場合:
Home(再レンダリングなし)
└─ NameDisplayUseWatch 🔁(再レンダリング)

僕は正直今までwatch()しか使ったことがありませんでした...
これからはuseWatch()を積極に使いつつ、フォーム全体の値を使う時のみwatch()を使うようにしたいと思います。

参考リンク

https://react-hook-form.com/docs/usewatch

Discussion