👀

React Hook Formに追加されたWatchコンポーネントの紹介

に公開

こんにちは 😀

先日、React Hook Form の公式 X から以下のポストがされました。

https://x.com/HookForm/status/1999233676912333108?s=20

投稿では Watch コンポーネントについて以下のような言及がされています。

  • form の値を監視する宣言的で非常にシンプルな方法
  • 既存の useWatch が好きなら、この新しいコンポーネントもきっと気に入るはず

Watchコンポーネントは少し前に React Hook Form に追加されたコンポーネントのようです。今回は既存のフォーム監視の方法から、<Watch />の何が嬉しいのかというところまで見ていきます!

watch とは?

まずは、React Hook Form の watch 機能について簡単に説明します。

watch はフォームの入力値を監視してその値を返す機能です。

よく使われるユースケースとしては:

  • 条件付きフィールド表示: 選択した値に応じて次の入力項目を表示
  • リアルタイムプレビュー: 入力内容を別の場所に即座に表示
  • 動的な計算: 複数のフィールドの値を組み合わせて計算結果を表示
  • フォーム進捗表示: 入力状況に応じて進捗バーを更新

といった感じで、ユーザーの入力に応じて UI を動的に変化させたいときに使います!

watch 機能の 3 つの使用方法

React Hook Form では、watch を使用する方法が 3 つあります:

  1. watch メソッド: useFormから取得する基本的な方法
  2. useWatch フック: パフォーマンス最適化のためのフック
  3. Watch コンポーネント: v7.65.0 で追加された新しい実装方法

それぞれの特徴と使い分けを、実装例を交えて見ていきたいと思います。

1. watch メソッド

まずは一番シンプルなwatchメソッドで作った form の挙動を見たいと思います。(React Developer Toolsを使ってレンダリングの様子を可視化しています)

watchメソッド使用時の再レンダリングの様子のキャプチャ

実装コード
WatchMethodExample.tsx
"use client";

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

type FormData = {
  name: string;
  email: string;
};

export function WatchMethodExample() {
  const { register, watch } = useForm<FormData>({
    defaultValues: {
      name: "",
      email: "",
    },
  });

  // watchメソッドで監視(このコンポーネント全体が再レンダリング)
  const [watchName, watchEmail] = watch(["name", "email"]);

  return (
    <div className="space-y-6">
      <div className="space-y-4">
        <div>
          <label
            htmlFor="name"
            className="block text-sm font-medium text-gray-700 mb-1"
          >
            名前
          </label>
          <input
            {...register("name")}
            className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
            placeholder="山田太郎"
            autoComplete="off"
          />
        </div>

        <div>
          <label
            htmlFor="email"
            className="block text-sm font-medium text-gray-700 mb-1"
          >
            メールアドレス
          </label>
          <input
            type="email"
            {...register("email")}
            className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
            placeholder="example@example.com"
            autoComplete="off"
          />
        </div>
      </div>

      {/* リアルタイムプレビュー */}
      <div className="bg-gray-50 border border-gray-200 rounded-lg p-4">
        <h3 className="text-sm font-semibold text-gray-700 mb-2">
          入力内容のプレビュー
        </h3>
        <div className="space-y-1 text-sm text-gray-600">
          <p>名前: {watchName || "(未入力)"}</p>
          <p>メール: {watchEmail || "(未入力)"}</p>
        </div>
      </div>
    </div>
  );
}

キャプチャを見ると分かると思いますが、watchメソッドを使うと、監視しているフィールドが変更されるたびにコンポーネント全体が再レンダリングされてしまいます。

小規模なフォームなら問題になりませんが、入力項目が多い大規模なフォームでは、この全体再レンダリングがパフォーマンスの問題になることがあります。

2. useWatch

watchメソッドのパフォーマンス問題を解決するのがuseWatchです。

useWatch使用時の再レンダリングの様子のキャプチャ

実装コード
UseWatchExample.tsx
"use client";

import { type Control, useForm, useWatch } from "react-hook-form";

type FormData = {
  name: string;
  email: string;
};

// 名前プレビューコンポーネント(名前が変更されたときだけ再レンダリング)
function NamePreview({ control }: { control: Control<FormData> }) {
  const name = useWatch({ control, name: "name" });

  return <p>名前: {name || "(未入力)"}</p>;
}

// メールプレビューコンポーネント(メールが変更されたときだけ再レンダリング)
function EmailPreview({ control }: { control: Control<FormData> }) {
  const email = useWatch({ control, name: "email" });

  return <p>メール: {email || "(未入力)"}</p>;
}

export function UseWatchExample() {
  const { register, control } = useForm<FormData>({
    defaultValues: {
      name: "",
      email: "",
    },
  });

  return (
    <div className="space-y-6">
      <div className="space-y-4">
        <div>
          <label
            htmlFor="name"
            className="block text-sm font-medium text-gray-700 mb-1"
          >
            名前
          </label>
          <input
            {...register("name")}
            className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
            placeholder="山田太郎"
            autoComplete="off"
          />
        </div>

        <div>
          <label
            htmlFor="email"
            className="block text-sm font-medium text-gray-700 mb-1"
          >
            メールアドレス
          </label>
          <input
            type="email"
            {...register("email")}
            className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
            placeholder="example@example.com"
            autoComplete="off"
          />
        </div>
      </div>

      {/* 別コンポーネントに分離されたプレビュー */}
      <div className="bg-gray-50 border border-gray-200 rounded-lg p-4">
        <h3 className="text-sm font-semibold text-gray-700 mb-2">
          入力内容のプレビュー
        </h3>
        <div className="space-y-1 text-sm text-gray-600">
          <NamePreview control={control} />
          <EmailPreview control={control} />
        </div>
      </div>
    </div>
  );
}

useWatchを使うことで、変更されたフィールドを使っているコンポーネントだけが再レンダリングされるようになります!

名前を入力したときは名前のプレビューエリアだけが、メールを入力したときはメールのプレビューエリアだけが更新されるので、無駄な再レンダリングが発生しません。

ただし、useWatchは React のフックなので、細かなパフォーマンスチューニングをしようとすると別のコンポーネントに分離する必要があったり、Hook のルールの制約を受ける使いにくさがあったりします。

3. Watch コンポーネント

そして今回の本題、<Watch />を使った form は以下のようになります。

Watchコンポーネント使用時の再レンダリングの様子のキャプチャ

実装コード
WatchComponentExample.tsx
"use client";

import { useForm, Watch } from "react-hook-form";

type FormData = {
  name: string;
  email: string;
};

export function WatchComponentExample() {
  const { register, control } = useForm<FormData>({
    defaultValues: {
      name: "",
      email: "",
    },
  });

  return (
    <div className="space-y-6">
      <div className="space-y-4">
        <div>
          <label
            htmlFor="name"
            className="block text-sm font-medium text-gray-700 mb-1"
          >
            名前
          </label>
          <input
            {...register("name")}
            className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500"
            placeholder="山田太郎"
            autoComplete="off"
          />
        </div>

        <div>
          <label
            htmlFor="email"
            className="block text-sm font-medium text-gray-700 mb-1"
          >
            メールアドレス
          </label>
          <input
            type="email"
            {...register("email")}
            className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500"
            placeholder="example@example.com"
            autoComplete="off"
          />
        </div>
      </div>

      {/* Watchコンポーネントで監視 */}
      <div className="bg-gray-50 border border-gray-200 rounded-lg p-4">
        <h3 className="text-sm font-semibold text-gray-700 mb-2">
          入力内容のプレビュー
        </h3>
        <div className="space-y-1 text-sm text-gray-600">
          {/* 名前だけを監視 */}
          <Watch
            control={control}
            names={["name"]}
            render={([name]) => <p>名前: {name || "(未入力)"}</p>}
          />
          {/* メールだけを監視 */}
          <Watch
            control={control}
            names={["email"]}
            render={([email]) => <p>メール: {email || "(未入力)"}</p>}
          />
        </div>
      </div>
    </div>
  );
}

<Watch />を使うと、useWatchと同じパフォーマンス最適化の恩恵を受けながら、コンポーネントを分離する必要がありません!

注目する点は以下の部分です:

  • control: useForm から取得した control オブジェクトを渡します
  • names: 監視したいフィールド名を配列で指定します
  • render: レンダリング関数を定義します。監視しているフィールドの値が配列で渡されます

この実装は Render Prop パターンと呼ばれるもので、一見するとコンポーネントを Props に渡していて奇妙に思えるかもしれませんが、ロジックの再利用と UI の柔軟性を両立できる良さがあります。

https://zenn.dev/morinokami/books/learning-patterns-1/viewer/render-props-pattern

useWatch と Watch コンポーネントは何が違うの?

一方で、キャプチャを見るとuseWatch<Watch />の実装とでパフォーマンス的に違いはないように思えます。したがって「useWatchでもWatchコンポーネントでも同じことができるなら、どっちを使えばいいの?」と思うかもしれません。

そこで考えるのが、useWatchのところで言及したHook のルールについてです。

useWatch の制約

useWatchは Hook なので、例えば、「ループや条件分岐の中で呼び出してはいけない」というルールに引っかかります。

したがって、動的に追加されるフィールドをループで表示したい場合は以下のように書くことはできずエラーになります。

// ❌ useWatchではこれができない(Hookのルール違反)
{
  fields.map((field, index) => {
    const value = useWatch({ control, name: `items.${index}.name` }); // エラー!
    return <div>{value}</div>;
  });
}

該当部分をコンポーネントとして切り出してその中でuseWatchを呼べば解決するのですが、たったこれだけのために新しくコンポーネントを作るのも微妙です。

Watch コンポーネントの柔軟性

一方、Watchコンポーネントはただの React コンポーネントなので、ループの中でも自由に使えます!

// ✅ Watchコンポーネントならできる
{
  fields.map((field, index) => (
    <Watch
      control={control}
      names={[`items.${index}.name`]}
      render={([value]) => <div>{value}</div>}
    />
  ));
}

また、useWatchだと条件分岐の中で呼び出すこともできませんが:

// ❌ useWatchでこれはできない
function Preview({ control, showName, showEmail }) {
  if (showName) {
    const name = useWatch({ control, name: 'name' }); // エラー!
    return <p>{name}</p>;
  }
  if (showEmail) {
    const email = useWatch({ control, name: 'email' }); // エラー!
    return <p>{email}</p>;
  }s
  return null;
}

<Watch />なら問題ありません:

// ✅ Watchコンポーネントなら問題なし
function Preview({ control, showName, showEmail }) {
  return (
    <>
      {showName && <Watch control={control} names={['name']} render={([name]) => <p>{name}</p>} />}
      {showEmail && (
        <Watch control={control} names={['email']} render={([email]) => <p>{email}</p>} />
      )}
    </>
  );
}

3 つの手法の使い分け

最後に、それぞれの手法の特徴についてまとめたいと思います!

watch メソッド

  • 使う場面: 小規模なフォーム、プロトタイプ開発
  • メリット: シンプルで書きやすい
  • デメリット: コンポーネント全体が再レンダリングされる

useWatch

  • 使う場面: 大規模フォームでパフォーマンスが気になる場合
  • メリット: 必要な部分だけ再レンダリング
  • デメリット: コンポーネント分離が必要、フックのルールに従う必要がある

Watch コンポーネント

  • 使う場面: パフォーマンスも柔軟性も欲しい場合
  • メリット: useWatch と同じパフォーマンス、コンポーネント分離不要、ループや条件分岐で使える
  • デメリット: Render Props パターンに慣れが必要

個人的には、パフォーマンスが気になるけど別コンポーネントにするほどでもない場合は、積極的に Watch コンポーネントを使っていきたいなと感じました!

最後に

今回は React Hook Form に追加された<Watch />について、watchメソッドやuseWatchとの違いを含めて紹介しました。

React Hook Form ってやりたいことに対して実装パターンがいくつかあって、最適な実装パターンを判断するのが大変っていうイメージなので、今回は X のポストをきっかけにそのあたりの知識整理ができて良い機会でした。

それでは、最後まで読んでいただきありがとうございました!👋

参考

https://react-hook-form.com/docs/useform/watch

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

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

https://github.com/react-hook-form/react-hook-form/pull/12986

GitHubで編集を提案

Discussion