React Hook Form watchの使い方|入力値の変化を監視する

React Hook Formのwatch機能の基本的な使い方から応用テクニックまで詳しく解説。入力値の変化を効率的に監視し、動的なフォームを作成する方法を学べます。

Learning Next 運営
36 分で読めます

みなさん、React Hook Formを使っていますか?

「入力値の変化に応じて他のフィールドを更新したい」「ユーザーが入力している内容をリアルタイムで表示したい」と思ったことはありませんか?

この記事では、React Hook Formのwatch機能を使って入力値の変化を監視する方法をお伝えします。 基本的な使い方から実践的な活用例まで、分かりやすく解説していきますね。

watchって何だろう?

まずは、watchがどんな機能なのかを理解しましょう。

watchの基本的な役割

watchは、React Hook Formが提供する入力値の変化を監視する機能です。

簡単に言うと、「フォームの入力値を見張って、変化があったら教えてくれる機能」なんです。

例えば、こんなことができます:

  • 名前を入力すると、自動で挨拶文が表示される
  • 商品の数量と単価を入力すると、合計金額が自動計算される
  • 住所を入力すると、送料が自動で更新される

従来の方法との違い

今までの方法と比べてみましょう:

// ❌ 従来の方法(useStateを使用)
import React, { useState } from 'react';

function TraditionalForm() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [preview, setPreview] = useState('');

  const handleNameChange = (e) => {
    const newName = e.target.value;
    setName(newName);
    setPreview(`こんにちは、${newName}さん`);
  };

  return (
    <form>
      <input
        value={name}
        onChange={handleNameChange}
        placeholder="名前"
      />
      <input
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="メールアドレス"
      />
      <div>プレビュー: {preview}</div>
    </form>
  );
}

従来の方法では、入力値が変わるたびに手動で状態を更新する必要がありました。 これだと、コードが複雑になりがちです。

// ✅ React Hook Formのwatchを使用
import { useForm } from 'react-hook-form';

function HookFormWithWatch() {
  const { register, watch } = useForm();
  
  const name = watch('name');
  
  return (
    <form>
      <input
        {...register('name')}
        placeholder="名前"
      />
      <input
        {...register('email')}
        placeholder="メールアドレス"
      />
      <div>プレビュー: こんにちは、{name}さん</div>
    </form>
  );
}

watchを使うと、とてもシンプルに書けますね! 入力値の変化を自動で追跡してくれるので、手動で状態管理する必要がありません。

基本的なwatchの使い方

それでは、実際にwatchを使ってみましょう。

一つのフィールドを監視

まずは、一つのフィールドの値を監視する基本的な例から:

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

function BasicWatchExample() {
  const { register, watch, handleSubmit } = useForm({
    defaultValues: {
      firstName: '',
      lastName: '',
      email: ''
    }
  });

  const firstName = watch('firstName');
  const email = watch('email');

  const onSubmit = (data) => {
    console.log('送信データ:', data);
  };

  return (
    <div>
      <h2>基本的なwatch使用例</h2>
      <form onSubmit={handleSubmit(onSubmit)}>
        <div>
          <label>名前:</label>
          <input
            {...register('firstName')}
            placeholder="名前を入力"
          />
        </div>

        <div>
          <label>姓:</label>
          <input
            {...register('lastName')}
            placeholder="姓を入力"
          />
        </div>

        <div>
          <label>メールアドレス:</label>
          <input
            {...register('email')}
            type="email"
            placeholder="メールアドレスを入力"
          />
        </div>

        <button type="submit">送信</button>
      </form>

      <div style={{ marginTop: '20px', padding: '15px', backgroundColor: '#f5f5f5' }}>
        <h3>リアルタイムプレビュー</h3>
        <p>名前: {firstName}</p>
        <p>メールアドレス: {email}</p>
        {firstName && (
          <p>こんにちは、{firstName}さん!</p>
        )}
      </div>
    </div>
  );
}

このコードでは、watch('firstName')watch('email')で特定のフィールドを監視しています。 入力値が変わると、すぐにプレビュー部分が更新されます。

複数のフィールドを同時に監視

複数のフィールドをまとめて監視することもできます:

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

function MultipleFieldsWatch() {
  const { register, watch, handleSubmit } = useForm({
    defaultValues: {
      product: '',
      quantity: 1,
      price: 0,
      discount: 0
    }
  });

  const [product, quantity, price, discount] = watch(['product', 'quantity', 'price', 'discount']);
  
  const subtotal = quantity * price;
  const discountAmount = subtotal * (discount / 100);
  const total = subtotal - discountAmount;

  const onSubmit = (data) => {
    console.log('注文データ:', { ...data, total });
  };

  return (
    <div>
      <h2>複数フィールド監視例</h2>
      <form onSubmit={handleSubmit(onSubmit)}>
        <div>
          <label>商品名:</label>
          <input
            {...register('product')}
            placeholder="商品名を入力"
          />
        </div>

        <div>
          <label>数量:</label>
          <input
            {...register('quantity', { valueAsNumber: true })}
            type="number"
            min="1"
          />
        </div>

        <div>
          <label>単価:</label>
          <input
            {...register('price', { valueAsNumber: true })}
            type="number"
            min="0"
            step="0.01"
          />
        </div>

        <div>
          <label>割引率(%):</label>
          <input
            {...register('discount', { valueAsNumber: true })}
            type="number"
            min="0"
            max="100"
          />
        </div>

        <button type="submit">注文する</button>
      </form>

      <div style={{ marginTop: '20px', padding: '15px', border: '1px solid #ddd' }}>
        <h3>注文内容</h3>
        <p>商品: {product}</p>
        <p>数量: {quantity}</p>
        <p>単価: ¥{price.toLocaleString()}</p>
        <p>小計: ¥{subtotal.toLocaleString()}</p>
        <p>割引: {discount}% (¥{discountAmount.toLocaleString()})</p>
        <p><strong>合計: ¥{total.toLocaleString()}</strong></p>
      </div>
    </div>
  );
}

watch(['product', 'quantity', 'price', 'discount'])で、複数のフィールドを配列で指定しています。 これで、どのフィールドが変わっても自動で計算結果が更新されます。

全フィールドを監視

フォーム全体の入力状況を把握したい場合は、全フィールドを監視できます:

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

function AllFieldsWatch() {
  const { register, watch, handleSubmit } = useForm({
    defaultValues: {
      name: '',
      email: '',
      phone: '',
      address: '',
      notes: ''
    }
  });

  const allValues = watch();

  const filledFields = Object.values(allValues).filter(value => 
    value && value.toString().trim() !== ''
  ).length;
  
  const totalFields = Object.keys(allValues).length;
  const completionPercentage = Math.round((filledFields / totalFields) * 100);

  const onSubmit = (data) => {
    console.log('全データ:', data);
  };

  return (
    <div>
      <h2>入力進捗を表示する例</h2>
      
      <div style={{ marginBottom: '20px', padding: '10px', backgroundColor: '#e3f2fd' }}>
        <h4>入力進捗: {completionPercentage}%</h4>
        <div style={{ 
          width: '100%', 
          backgroundColor: '#ddd', 
          borderRadius: '5px',
          height: '10px'
        }}>
          <div style={{
            width: `${completionPercentage}%`,
            backgroundColor: '#2196f3',
            height: '100%',
            borderRadius: '5px',
            transition: 'width 0.3s ease'
          }}></div>
        </div>
        <p>{filledFields} / {totalFields} フィールド入力済み</p>
      </div>

      <form onSubmit={handleSubmit(onSubmit)}>
        <div>
          <label>名前:</label>
          <input
            {...register('name')}
            placeholder="名前を入力"
          />
        </div>

        <div>
          <label>メールアドレス:</label>
          <input
            {...register('email')}
            type="email"
            placeholder="メールアドレスを入力"
          />
        </div>

        <div>
          <label>電話番号:</label>
          <input
            {...register('phone')}
            type="tel"
            placeholder="電話番号を入力"
          />
        </div>

        <div>
          <label>住所:</label>
          <input
            {...register('address')}
            placeholder="住所を入力"
          />
        </div>

        <div>
          <label>備考:</label>
          <textarea
            {...register('notes')}
            placeholder="備考があれば入力"
            rows="3"
          />
        </div>

        <button 
          type="submit"
          disabled={completionPercentage < 100}
          style={{
            backgroundColor: completionPercentage === 100 ? '#4caf50' : '#ccc',
            color: 'white',
            padding: '10px 20px',
            border: 'none',
            borderRadius: '5px',
            cursor: completionPercentage === 100 ? 'pointer' : 'not-allowed'
          }}
        >
          {completionPercentage === 100 ? '送信' : `あと${totalFields - filledFields}項目`}
        </button>
      </form>
    </div>
  );
}

watch()(引数なし)で、フォーム全体の値を監視できます。 入力進捗バーを表示して、ユーザーにどのくらい入力が完了しているかを伝えられますね。

条件によって表示を切り替える

watchを使えば、入力値に応じて動的にフィールドを表示・非表示できます。

基本的な条件付き表示

選択した内容によって、追加のフィールドを表示する例を見てみましょう:

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

function ConditionalFields() {
  const { register, watch, handleSubmit } = useForm({
    defaultValues: {
      accountType: '',
      companyName: '',
      taxId: '',
      personalId: '',
      hasDelivery: false,
      deliveryAddress: '',
      deliveryNotes: ''
    }
  });

  const accountType = watch('accountType');
  const hasDelivery = watch('hasDelivery');

  const onSubmit = (data) => {
    console.log('送信データ:', data);
  };

  return (
    <div>
      <h2>条件付きフィールド表示</h2>
      <form onSubmit={handleSubmit(onSubmit)}>
        <div>
          <label>アカウントタイプ:</label>
          <select {...register('accountType')}>
            <option value="">選択してください</option>
            <option value="personal">個人</option>
            <option value="business">法人</option>
          </select>
        </div>

        {accountType === 'business' && (
          <div style={{ backgroundColor: '#f0f8ff', padding: '15px', margin: '10px 0' }}>
            <h4>法人情報</h4>
            <div>
              <label>会社名:</label>
              <input
                {...register('companyName', { 
                  required: accountType === 'business' ? '会社名は必須です' : false 
                })}
                placeholder="会社名を入力"
              />
            </div>
            <div>
              <label>法人番号:</label>
              <input
                {...register('taxId')}
                placeholder="法人番号を入力"
              />
            </div>
          </div>
        )}

        {accountType === 'personal' && (
          <div style={{ backgroundColor: '#f0fff0', padding: '15px', margin: '10px 0' }}>
            <h4>個人情報</h4>
            <div>
              <label>個人番号:</label>
              <input
                {...register('personalId')}
                placeholder="個人番号を入力"
              />
            </div>
          </div>
        )}

        <div>
          <label>
            <input
              {...register('hasDelivery')}
              type="checkbox"
            />
            配送が必要
          </label>
        </div>

        {hasDelivery && (
          <div style={{ backgroundColor: '#fff8f0', padding: '15px', margin: '10px 0' }}>
            <h4>配送情報</h4>
            <div>
              <label>配送先住所:</label>
              <textarea
                {...register('deliveryAddress', { 
                  required: hasDelivery ? '配送先住所は必須です' : false 
                })}
                placeholder="配送先住所を入力"
                rows="3"
              />
            </div>
            <div>
              <label>配送時の注意事項:</label>
              <textarea
                {...register('deliveryNotes')}
                placeholder="配送時の注意事項があれば入力"
                rows="2"
              />
            </div>
          </div>
        )}

        <button type="submit">送信</button>
      </form>
    </div>
  );
}

このコードでは、以下の条件分岐を行っています:

  • accountTypeが「business」の時:法人情報フィールドを表示
  • accountTypeが「personal」の時:個人情報フィールドを表示
  • hasDeliveryがチェックされた時:配送情報フィールドを表示

ユーザーの選択に応じて、必要なフィールドだけが表示されるので、とても使いやすいフォームになります。

リアルタイムバリデーション

watchを使って、リアルタイムでバリデーションを行うこともできます。

パスワード確認の例

パスワードと確認パスワードをリアルタイムで照合する例を見てみましょう:

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

function PasswordValidationForm() {
  const { register, watch, handleSubmit, formState: { errors } } = useForm({
    defaultValues: {
      username: '',
      password: '',
      confirmPassword: '',
      email: ''
    }
  });

  const password = watch('password');
  const confirmPassword = watch('confirmPassword');

  const getPasswordStrength = (pwd) => {
    if (!pwd) return { score: 0, text: '', color: '#ccc' };
    
    let score = 0;
    let feedback = [];

    if (pwd.length >= 8) score += 1;
    else feedback.push('8文字以上');

    if (/[a-z]/.test(pwd)) score += 1;
    else feedback.push('小文字');

    if (/[A-Z]/.test(pwd)) score += 1;
    else feedback.push('大文字');

    if (/[0-9]/.test(pwd)) score += 1;
    else feedback.push('数字');

    if (/[^a-zA-Z0-9]/.test(pwd)) score += 1;
    else feedback.push('記号');

    const strengthLevels = [
      { text: '非常に弱い', color: '#e74c3c' },
      { text: '弱い', color: '#f39c12' },
      { text: '普通', color: '#f1c40f' },
      { text: '強い', color: '#27ae60' },
      { text: '非常に強い', color: '#2ecc71' }
    ];

    return {
      score,
      text: strengthLevels[score] ? strengthLevels[score].text : '非常に弱い',
      color: strengthLevels[score] ? strengthLevels[score].color : '#e74c3c',
      missing: feedback
    };
  };

  const passwordStrength = getPasswordStrength(password);
  const passwordsMatch = password && confirmPassword && password === confirmPassword;
  const passwordsNotMatch = password && confirmPassword && password !== confirmPassword;

  const onSubmit = (data) => {
    console.log('ユーザー登録データ:', data);
  };

  return (
    <div>
      <h2>リアルタイムパスワードバリデーション</h2>
      <form onSubmit={handleSubmit(onSubmit)}>
        <div>
          <label>ユーザー名:</label>
          <input
            {...register('username', { 
              required: 'ユーザー名は必須です',
              minLength: { value: 3, message: '3文字以上で入力してください' }
            })}
            placeholder="ユーザー名を入力"
          />
          {errors.username && (
            <p style={{ color: '#e74c3c', fontSize: '14px' }}>
              {errors.username.message}
            </p>
          )}
        </div>

        <div>
          <label>パスワード:</label>
          <input
            {...register('password', { 
              required: 'パスワードは必須です',
              minLength: { value: 8, message: '8文字以上で入力してください' }
            })}
            type="password"
            placeholder="パスワードを入力"
          />
          {errors.password && (
            <p style={{ color: '#e74c3c', fontSize: '14px' }}>
              {errors.password.message}
            </p>
          )}
          
          {password && (
            <div style={{ marginTop: '5px' }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
                <span>強度:</span>
                <div style={{
                  width: '100px',
                  height: '10px',
                  backgroundColor: '#ddd',
                  borderRadius: '5px',
                  overflow: 'hidden'
                }}>
                  <div style={{
                    width: `${(passwordStrength.score / 5) * 100}%`,
                    height: '100%',
                    backgroundColor: passwordStrength.color,
                    transition: 'all 0.3s ease'
                  }}></div>
                </div>
                <span style={{ color: passwordStrength.color, fontSize: '14px' }}>
                  {passwordStrength.text}
                </span>
              </div>
              {passwordStrength.missing.length > 0 && (
                <p style={{ fontSize: '12px', color: '#666', margin: '5px 0' }}>
                  改善点: {passwordStrength.missing.join('、')}を含める
                </p>
              )}
            </div>
          )}
        </div>

        <div>
          <label>パスワード確認:</label>
          <input
            {...register('confirmPassword', {
              required: 'パスワード確認は必須です',
              validate: (value) => 
                value === password || 'パスワードが一致しません'
            })}
            type="password"
            placeholder="パスワードを再入力"
          />
          {errors.confirmPassword && (
            <p style={{ color: '#e74c3c', fontSize: '14px' }}>
              {errors.confirmPassword.message}
            </p>
          )}
          
          {confirmPassword && (
            <div style={{ marginTop: '5px' }}>
              {passwordsMatch && (
                <p style={{ color: '#27ae60', fontSize: '14px', margin: 0 }}>
                  ✓ パスワードが一致しています
                </p>
              )}
              {passwordsNotMatch && (
                <p style={{ color: '#e74c3c', fontSize: '14px', margin: 0 }}>
                  ✗ パスワードが一致しません
                </p>
              )}
            </div>
          )}
        </div>

        <button 
          type="submit"
          disabled={!passwordsMatch || passwordStrength.score < 3}
          style={{
            backgroundColor: passwordsMatch && passwordStrength.score >= 3 ? '#27ae60' : '#ccc',
            color: 'white',
            padding: '10px 20px',
            border: 'none',
            borderRadius: '5px',
            cursor: passwordsMatch && passwordStrength.score >= 3 ? 'pointer' : 'not-allowed',
            marginTop: '10px'
          }}
        >
          アカウント作成
        </button>
      </form>
    </div>
  );
}

このコードでは、以下のリアルタイムバリデーションを行っています:

  1. パスワード強度チェック:文字数、大文字・小文字・数字・記号の有無を確認
  2. パスワード一致確認:パスワードと確認パスワードが同じかチェック
  3. 視覚的フィードバック:強度バーや一致状況をリアルタイム表示

ユーザーが入力しながら、即座にフィードバックを得られるので、とても使いやすいフォームになります。

パフォーマンスを考慮した使い方

watchを効率的に使うためのポイントを見てみましょう。

必要な部分だけを監視

import React, { useMemo } from 'react';
import { useForm } from 'react-hook-form';

function OptimizedWatchForm() {
  const { register, watch, handleSubmit } = useForm({
    defaultValues: {
      firstName: '',
      lastName: '',
      email: '',
      age: 0,
      preferences: {
        newsletter: false,
        notifications: false
      }
    }
  });

  // ✅ 良い例:必要なフィールドのみを監視
  const firstName = watch('firstName');
  const lastName = watch('lastName');
  
  // ✅ 良い例:複数フィールドをまとめて監視
  const [email, age] = watch(['email', 'age']);
  
  // ❌ 悪い例:全フィールドを監視(不要な再レンダリングが発生)
  // const allValues = watch();

  const fullName = useMemo(() => {
    if (!firstName && !lastName) return '';
    return `${firstName} ${lastName}`.trim();
  }, [firstName, lastName]);

  const onSubmit = (data) => {
    console.log('最適化されたフォームデータ:', data);
  };

  return (
    <div>
      <h2>最適化されたwatchの使用例</h2>
      <form onSubmit={handleSubmit(onSubmit)}>
        <div>
          <label>名前:</label>
          <input
            {...register('firstName')}
            placeholder="名前を入力"
          />
        </div>

        <div>
          <label>姓:</label>
          <input
            {...register('lastName')}
            placeholder="姓を入力"
          />
        </div>

        <div>
          <label>メールアドレス:</label>
          <input
            {...register('email')}
            type="email"
            placeholder="メールアドレスを入力"
          />
        </div>

        <button type="submit">送信</button>
      </form>

      <div style={{ marginTop: '20px', padding: '15px', backgroundColor: '#f5f5f5' }}>
        <h3>プロフィールプレビュー</h3>
        {fullName && <p>氏名: {fullName}</p>}
        {email && <p>メールアドレス: {email}</p>}
      </div>
    </div>
  );
}

最適化のポイント

  • 必要なフィールドのみを監視する
  • 計算結果はuseMemoでメモ化する
  • 全フィールド監視(watch())は必要な時のみ使用する

これらのポイントを意識することで、パフォーマンスの良いフォームを作ることができます。

よくある問題と解決方法

watchを使う時によくある問題と、その解決方法をご紹介します。

デフォルト値の問題

問題: watchが初期値を返さない

// ❌ 悪い例(デフォルト値なし)
function BadDefaultExample() {
  const { register, watch } = useForm();
  const name = watch('name'); // undefinedになる可能性
  
  return <div>Hello, {name}!</div>; // "Hello, undefined!" になる
}

// ✅ 良い例(デフォルト値設定)
function GoodDefaultExample() {
  const { register, watch } = useForm({
    defaultValues: {
      name: 'ゲスト'
    }
  });
  const name = watch('name');
  
  return <div>Hello, {name}!</div>; // "Hello, ゲスト!" になる
}

解決方法: useFormdefaultValuesを設定するか、watchの第2引数でデフォルト値を指定しましょう。

パフォーマンスの問題

問題: watchを使いすぎて再レンダリングが頻発する

// ❌ 悪い例(不要な再レンダリング)
function BadExample() {
  const { register, watch } = useForm();
  const allValues = watch(); // 全フィールドを監視
  
  return (
    <div>
      <input {...register('field1')} />
      <input {...register('field2')} />
      <div>Field1: {allValues.field1}</div>
    </div>
  );
}

// ✅ 良い例(必要な部分のみ監視)
function GoodExample() {
  const { register, watch } = useForm();
  const field1 = watch('field1'); // 必要なフィールドのみ監視
  
  return (
    <div>
      <input {...register('field1')} />
      <input {...register('field2')} />
      <div>Field1: {field1}</div>
    </div>
  );
}

解決方法: 全フィールドではなく、必要なフィールドのみを監視しましょう。

まとめ

React Hook Formのwatch機能について、詳しく解説しました。

watchの主な利点

  • リアルタイムで入力値の変化を監視できる
  • 動的なフォーム表示を簡単に実装できる
  • パフォーマンスが良い(必要な部分のみ再レンダリング)
  • コードがシンプルで分かりやすい

効果的な活用方法

  • 条件付きフィールド表示
  • リアルタイムバリデーション
  • 自動計算機能
  • 入力プレビュー
  • 進捗表示

パフォーマンス最適化のコツ

  • 必要最小限のフィールドのみを監視
  • 計算結果はメモ化を活用
  • デフォルト値を適切に設定
  • 不要な全フィールド監視は避ける

watchを適切に使うことで、ユーザーフレンドリーで高性能なフォームを作ることができます。 ぜひ実際のプロジェクトで活用してみてくださいね!

関連記事