React Hook Form watchの使い方|入力値の変化を監視する
React Hook Formのwatch機能の基本的な使い方から応用テクニックまで詳しく解説。入力値の変化を効率的に監視し、動的なフォームを作成する方法を学べます。
みなさん、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>
);
}
このコードでは、以下のリアルタイムバリデーションを行っています:
- パスワード強度チェック:文字数、大文字・小文字・数字・記号の有無を確認
- パスワード一致確認:パスワードと確認パスワードが同じかチェック
- 視覚的フィードバック:強度バーや一致状況をリアルタイム表示
ユーザーが入力しながら、即座にフィードバックを得られるので、とても使いやすいフォームになります。
パフォーマンスを考慮した使い方
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, ゲスト!" になる
}
解決方法: useForm
でdefaultValues
を設定するか、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を適切に使うことで、ユーザーフレンドリーで高性能なフォームを作ることができます。 ぜひ実際のプロジェクトで活用してみてくださいね!