React Hook Form入門|フォーム実装が驚くほど簡単になる方法

React Hook Formを使って複雑なフォーム処理を簡単に実装する方法を解説。バリデーション、パフォーマンス最適化、実践的な使い方まで詳しく紹介

Learning Next 運営
52 分で読めます

みなさん、Reactでフォームを作るとき「バリデーションが面倒」と悩んでいませんか?

「再レンダリングが多すぎて重い」 「エラーハンドリングでコードが複雑になる」 「状態管理だけで数百行になってしまう」

こんな経験をしたことがある方も多いはずです。

実は、React Hook Formを使えばこれらの問題を一気に解決できます。 この記事では、React Hook Formを使って驚くほど簡単にフォームを実装する方法を詳しく解説します。

パフォーマンス最適化から実践的な使い方まで、すぐに使える内容をお伝えしますよ。 ぜひ最後まで読んで、効率的なフォーム開発をマスターしてくださいね!

React Hook Formって何?基本を理解しよう

React Hook Formは、フォーム処理に特化したReactライブラリです。 従来のフォーム実装と比べて、驚くほど簡単にフォームを作ることができるんです。

従来の方法と何が違うの?

まず、従来のフォーム実装と比較してみましょう。 違いを見れば、React Hook Formの便利さがすぐに分かりますよ。

従来のReactフォーム(useState使用)の場合

// ❌ 従来のReactフォーム(useState使用)
import { useState } from 'react';

function TraditionalForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    age: ''
  });
  const [errors, setErrors] = useState({});

  const handleChange = (e) => {
    // 入力のたびに再レンダリング
    setFormData({
      ...formData,
      [e.target.name]: e.target.value
    });
  };

  const validateForm = () => {
    const newErrors = {};
    if (!formData.name) newErrors.name = '名前は必須です';
    if (!formData.email) newErrors.email = 'メールアドレスは必須です';
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    if (validateForm()) {
      console.log('送信:', formData);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="name"
        value={formData.name}
        onChange={handleChange}
        placeholder="名前"
      />
      {errors.name && <span>{errors.name}</span>}
      {/* 他のフィールドも同様に... */}
    </form>
  );
}

なんだか長くて複雑ですよね。 特に、入力のたびに再レンダリングが発生するのが問題です。

React Hook Form使用の場合

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

function SimpleForm() {
  const {
    register,
    handleSubmit,
    formState: { errors }
  } = useForm();

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

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        {...register('name', { required: '名前は必須です' })}
        placeholder="名前"
      />
      {errors.name && <span>{errors.name.message}</span>}
      
      <input
        {...register('email', {
          required: 'メールアドレスは必須です',
          pattern: {
            value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
            message: 'メールアドレスの形式が正しくありません'
          }
        })}
        placeholder="メールアドレス"
      />
      {errors.email && <span>{errors.email.message}</span>}
      
      <button type="submit">送信</button>
    </form>
  );
}

すごくシンプルになりましたよね! コード量が大幅に削減されているのが分かります。

React Hook Formの魅力的な特徴

React Hook Formが多くの開発者に愛用される理由を見てみましょう。

最小限の再レンダリングでサクサク動く

従来の方法では入力のたびに再レンダリングが発生します。 でも、React Hook Formは必要最小限に抑えてくれるんです。

// 入力値の変更時に再レンダリングされない
function OptimizedForm() {
  console.log('レンダリング'); // 入力時に実行されない
  
  const { register, handleSubmit } = useForm();
  
  return (
    <form onSubmit={handleSubmit(console.log)}>
      <input {...register('message')} />
      <button>送信</button>
    </form>
  );
}

入力中にconsole.logが実行されないので、パフォーマンスが向上します。 これは大きなメリットですよね。

直感的で分かりやすいAPI

複雑なフォームも直感的に書けるのが魅力です。

// 複雑なバリデーションも簡潔に
const { register } = useForm();

<input
  {...register('password', {
    required: 'パスワードは必須です',
    minLength: {
      value: 8,
      message: '8文字以上で入力してください'
    },
    pattern: {
      value: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
      message: '大文字、小文字、数字を含む必要があります'
    }
  })}
/>

このように、バリデーションルールを分かりやすく書けます。 初心者でも理解しやすい構文ですね。

軽量でパフォーマンス重視

バンドルサイズが小さく、パフォーマンスに優れています。

他のフォームライブラリとの比較を見てみましょう。

  • React Hook Form: ~25KB
  • Formik: ~65KB
  • Redux Form: ~155KB

React Hook Formは圧倒的に軽量なんです。 アプリケーションの読み込み速度にも良い影響を与えますよ。

実際に使ってみよう!基本的な使い方

実際にReact Hook Formを使ってフォームを作ってみましょう。 まずは簡単な例から始めて、少しずつ機能を覚えていきますよ。

インストールと初期設定

まず、必要なパッケージをインストールします。 とても簡単です。

npm install react-hook-form
# または
yarn add react-hook-form

これだけでインストール完了です!

基本的なフォームを作ってみよう

最もシンプルなフォームから始めましょう。 まずは全体を見てから、一つずつ解説していきますね。

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

function BasicForm() {
  const { register, handleSubmit, formState: { errors } } = useForm();

  const onSubmit = (data) => {
    console.log('送信データ:', data);
    // APIコールなどの処理
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label>名前:</label>
        <input
          {...register('name', { required: '名前は必須です' })}
          type="text"
        />
        {errors.name && <p>{errors.name.message}</p>}
      </div>

      <div>
        <label>メールアドレス:</label>
        <input
          {...register('email', {
            required: 'メールアドレスは必須です',
            pattern: {
              value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
              message: '正しいメールアドレスを入力してください'
            }
          })}
          type="email"
        />
        {errors.email && <p>{errors.email.message}</p>}
      </div>

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

それでは、このコードを部分的に説明していきますね。

まず、useFormから必要な機能を取得します。

const { register, handleSubmit, formState: { errors } } = useForm();
  • register: 入力フィールドを登録する関数
  • handleSubmit: フォーム送信を処理する関数
  • errors: バリデーションエラーを格納するオブジェクト

次に、送信処理を定義します。

const onSubmit = (data) => {
  console.log('送信データ:', data);
  // APIコールなどの処理
};

dataには、フォームの全ての入力値がオブジェクトとして渡されます。 とても便利ですよね。

フィールドの登録も簡単です。

<input
  {...register('name', { required: '名前は必須です' })}
  type="text"
/>

register関数で、フィールド名とバリデーションルールを指定するだけ。 スプレッド演算子(...)で、必要な属性が自動的に設定されます。

フォームの状態をもっと細かく制御してみよう

フォームの状態を細かく制御することもできます。 より実践的な例を見てみましょう。

function StateManagementForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting, isDirty, isValid },
    reset,
    watch
  } = useForm({ mode: 'onChange' });

  // 特定のフィールドを監視
  const watchedName = watch('name');

  const onSubmit = async (data) => {
    // 非同期処理(API呼び出しなど)
    await new Promise(resolve => setTimeout(resolve, 1000));
    console.log('送信完了:', data);
    reset(); // フォームをリセット
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        {...register('name', { required: '名前は必須です' })}
        placeholder="名前"
      />
      {errors.name && <span>{errors.name.message}</span>}

      {/* 現在の入力値を表示 */}
      <p>現在の入力: {watchedName}</p>

      <button
        type="submit"
        disabled={!isValid || isSubmitting}
      >
        {isSubmitting ? '送信中...' : '送信'}
      </button>

      <button type="button" onClick={() => reset()}>
        リセット
      </button>
    </form>
  );
}

この例では、さらに多くの機能を使っています。

  • isSubmitting: 送信中かどうかの状態
  • isDirty: フォームが変更されたかどうか
  • isValid: フォームが有効かどうか
  • reset: フォームをリセットする関数
  • watch: 特定フィールドの値を監視する関数

これらを組み合わせることで、ユーザーフレンドリーなフォームが作れますよ。

デフォルト値を設定してみよう

フォームにデフォルト値を設定することも簡単です。 編集フォームなどでよく使う機能ですね。

function DefaultValueForm() {
  const { register, handleSubmit } = useForm({
    defaultValues: {
      name: '田中太郎',
      email: 'tanaka@example.com',
      age: 30,
      country: 'japan'
    }
  });

  return (
    <form onSubmit={handleSubmit(console.log)}>
      <input {...register('name')} />
      <input {...register('email')} />
      <input {...register('age', { valueAsNumber: true })} type="number" />
      
      <select {...register('country')}>
        <option value="japan">日本</option>
        <option value="usa">アメリカ</option>
        <option value="uk">イギリス</option>
      </select>
      
      <button type="submit">送信</button>
    </form>
  );
}

defaultValuesでまとめて初期値を設定できます。 数値フィールドにはvalueAsNumber: trueを指定すると、文字列ではなく数値として扱われますよ。

バリデーション機能を使いこなそう

React Hook Formの強力なバリデーション機能を詳しく見てみましょう。 様々なパターンのバリデーションを実装できるんです。

基本的なバリデーションパターン

まずは、よく使われる基本的なバリデーションを見てみましょう。

function ValidationForm() {
  const { register, handleSubmit, formState: { errors } } = useForm();

  return (
    <form onSubmit={handleSubmit(console.log)}>
      {/* 必須チェック */}
      <input
        {...register('required', { required: 'この項目は必須です' })}
        placeholder="必須項目"
      />
      {errors.required && <p>{errors.required.message}</p>}

      {/* 最小・最大文字数 */}
      <input
        {...register('minMax', {
          minLength: {
            value: 3,
            message: '3文字以上入力してください'
          },
          maxLength: {
            value: 10,
            message: '10文字以下で入力してください'
          }
        })}
        placeholder="3-10文字"
      />
      {errors.minMax && <p>{errors.minMax.message}</p>}

      {/* 数値の範囲 */}
      <input
        {...register('numberRange', {
          min: {
            value: 18,
            message: '18以上の数値を入力してください'
          },
          max: {
            value: 100,
            message: '100以下の数値を入力してください'
          }
        })}
        type="number"
        placeholder="18-100"
      />
      {errors.numberRange && <p>{errors.numberRange.message}</p>}

      {/* 正規表現 */}
      <input
        {...register('phone', {
          pattern: {
            value: /^0\d{2,3}-\d{1,4}-\d{4}$/,
            message: '電話番号の形式が正しくありません(例:03-1234-5678)'
          }
        })}
        placeholder="電話番号"
      />
      {errors.phone && <p>{errors.phone.message}</p>}

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

それぞれのバリデーションを詳しく見てみましょう。

必須チェック

register('required', { required: 'この項目は必須です' })

最もシンプルなバリデーションです。 空の場合にエラーメッセージが表示されます。

文字数の制限

register('minMax', {
  minLength: { value: 3, message: '3文字以上入力してください' },
  maxLength: { value: 10, message: '10文字以下で入力してください' }
})

最小・最大文字数を制限できます。 パスワードやユーザー名の長さ制限によく使いますね。

数値の範囲

register('numberRange', {
  min: { value: 18, message: '18以上の数値を入力してください' },
  max: { value: 100, message: '100以下の数値を入力してください' }
})

年齢や価格など、数値の範囲を制限する際に便利です。

正規表現による形式チェック

register('phone', {
  pattern: {
    value: /^0\d{2,3}-\d{1,4}-\d{4}$/,
    message: '電話番号の形式が正しくありません'
  }
})

メールアドレスや電話番号など、特定の形式をチェックできます。

カスタムバリデーションで独自のルールを作ろう

独自のバリデーションロジックも作成できます。 より複雑な検証が必要な場合に使いましょう。

function CustomValidationForm() {
  const { register, handleSubmit, formState: { errors } } = useForm();

  // カスタムバリデーション関数
  const validateUsername = (value) => {
    if (!value) return '必須項目です';
    if (value.length < 3) return '3文字以上で入力してください';
    if (!/^[a-zA-Z0-9_]+$/.test(value)) return '英数字とアンダースコアのみ使用可能です';
    return true;
  };

  const validatePassword = (value) => {
    if (!value) return '必須項目です';
    if (value.length < 8) return '8文字以上で入力してください';
    if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value)) {
      return '大文字、小文字、数字をそれぞれ1文字以上含む必要があります';
    }
    return true;
  };

  return (
    <form onSubmit={handleSubmit(console.log)}>
      <input
        {...register('username', { validate: validateUsername })}
        placeholder="ユーザー名"
      />
      {errors.username && <p>{errors.username.message}</p>}

      <input
        {...register('password', { validate: validatePassword })}
        type="password"
        placeholder="パスワード"
      />
      {errors.password && <p>{errors.password.message}</p>}

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

カスタムバリデーション関数では、以下のような処理ができます。

  • trueを返す:バリデーション成功
  • 文字列を返す:エラーメッセージとして表示

複数の条件を組み合わせた複雑な検証も可能ですよ。

非同期バリデーションでサーバーサイドチェック

サーバーサイドでのチェックも可能です。 ユーザー名の重複チェックなどで活用できますね。

function AsyncValidationForm() {
  const { register, handleSubmit, formState: { errors } } = useForm();

  // 非同期バリデーション(ユーザー名の重複チェック)
  const checkUsernameAvailability = async (username) => {
    if (!username) return '必須項目です';
    
    try {
      const response = await fetch(`/api/check-username/${username}`);
      const data = await response.json();
      return data.available || 'このユーザー名は既に使用されています';
    } catch (error) {
      return 'ユーザー名の確認に失敗しました';
    }
  };

  return (
    <form onSubmit={handleSubmit(console.log)}>
      <input
        {...register('username', {
          required: 'ユーザー名は必須です',
          validate: checkUsernameAvailability
        })}
        placeholder="ユーザー名"
      />
      {errors.username && <p>{errors.username.message}</p>}

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

非同期バリデーションでは、Promise を返す関数を指定します。 API呼び出しでリアルタイムに検証できるので、ユーザビリティが向上しますよ。

実践的なフォームを作ってみよう

実際のアプリケーションで使えるレベルのフォームを作成してみましょう。 より実践的な例をご紹介しますね。

ユーザー登録フォームを作ってみよう

本格的なユーザー登録フォームを実装してみます。 まずは全体のコードを見てから、詳しく解説していきますよ。

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

function UserRegistrationForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
    watch,
    reset
  } = useForm();

  const password = watch('password');

  const onSubmit = async (data) => {
    try {
      const response = await fetch('/api/register', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(data),
      });

      if (response.ok) {
        alert('登録が完了しました');
        reset();
      } else {
        alert('登録に失敗しました');
      }
    } catch (error) {
      console.error('Registration error:', error);
      alert('エラーが発生しました');
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="registration-form">
      <h2>ユーザー登録</h2>

      <div className="form-group">
        <label>名前 *</label>
        <input
          {...register('name', {
            required: '名前は必須です',
            minLength: {
              value: 2,
              message: '2文字以上で入力してください'
            }
          })}
          type="text"
          placeholder="田中太郎"
        />
        {errors.name && <span className="error">{errors.name.message}</span>}
      </div>

      <div className="form-group">
        <label>メールアドレス *</label>
        <input
          {...register('email', {
            required: 'メールアドレスは必須です',
            pattern: {
              value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
              message: '正しいメールアドレスを入力してください'
            }
          })}
          type="email"
          placeholder="example@mail.com"
        />
        {errors.email && <span className="error">{errors.email.message}</span>}
      </div>

      <div className="form-group">
        <label>パスワード *</label>
        <input
          {...register('password', {
            required: 'パスワードは必須です',
            minLength: {
              value: 8,
              message: '8文字以上で入力してください'
            },
            pattern: {
              value: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
              message: '大文字、小文字、数字を含む必要があります'
            }
          })}
          type="password"
          placeholder="パスワード"
        />
        {errors.password && <span className="error">{errors.password.message}</span>}
      </div>

      <div className="form-group">
        <label>パスワード確認 *</label>
        <input
          {...register('passwordConfirm', {
            required: 'パスワード確認は必須です',
            validate: (value) =>
              value === password || 'パスワードが一致しません'
          })}
          type="password"
          placeholder="パスワード(確認)"
        />
        {errors.passwordConfirm && <span className="error">{errors.passwordConfirm.message}</span>}
      </div>

      <div className="form-group">
        <label>生年月日</label>
        <input
          {...register('birthDate', {
            required: '生年月日は必須です',
            validate: (value) => {
              const today = new Date();
              const birthDate = new Date(value);
              const age = today.getFullYear() - birthDate.getFullYear();
              return age >= 18 || '18歳以上である必要があります';
            }
          })}
          type="date"
        />
        {errors.birthDate && <span className="error">{errors.birthDate.message}</span>}
      </div>

      <div className="form-group">
        <label>
          <input
            {...register('agreeTerms', {
              required: '利用規約への同意は必須です'
            })}
            type="checkbox"
          />
          利用規約に同意する *
        </label>
        {errors.agreeTerms && <span className="error">{errors.agreeTerms.message}</span>}
      </div>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? '登録中...' : '登録する'}
      </button>
    </form>
  );
}

このフォームの注目ポイントを説明しますね。

パスワード確認フィールド

const password = watch('password');

<input
  {...register('passwordConfirm', {
    validate: (value) => value === password || 'パスワードが一致しません'
  })}
/>

watchでパスワードフィールドを監視して、確認フィールドと比較しています。 リアルタイムでチェックできて便利ですよね。

年齢制限チェック

validate: (value) => {
  const today = new Date();
  const birthDate = new Date(value);
  const age = today.getFullYear() - birthDate.getFullYear();
  return age >= 18 || '18歳以上である必要があります';
}

生年月日から年齢を計算して、18歳以上かチェックしています。 カスタムバリデーションで複雑な条件も簡単に実装できますよ。

チェックボックスのバリデーション

<input
  {...register('agreeTerms', {
    required: '利用規約への同意は必須です'
  })}
  type="checkbox"
/>

チェックボックスでも必須チェックができます。 利用規約への同意確認などでよく使われますね。

動的フォームで柔軟性をアップ

フィールドを動的に追加・削除できるフォームも作成できます。 スキル一覧や連絡先リストなどで活用できますよ。

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

function DynamicForm() {
  const { register, control, handleSubmit, formState: { errors } } = useForm({
    defaultValues: {
      skills: [{ name: '', level: 1 }]
    }
  });

  const { fields, append, remove } = useFieldArray({
    control,
    name: 'skills'
  });

  const addSkill = () => {
    append({ name: '', level: 1 });
  };

  const removeSkill = (index) => {
    remove(index);
  };

  return (
    <form onSubmit={handleSubmit(console.log)}>
      <h3>スキル一覧</h3>

      {fields.map((field, index) => (
        <div key={field.id} className="skill-item">
          <input
            {...register(`skills.${index}.name`, {
              required: 'スキル名は必須です'
            })}
            placeholder="スキル名"
          />
          {errors.skills?.[index]?.name && (
            <span>{errors.skills[index].name.message}</span>
          )}

          <select
            {...register(`skills.${index}.level`, {
              valueAsNumber: true
            })}
          >
            <option value={1}>初級</option>
            <option value={2}>中級</option>
            <option value={3}>上級</option>
          </select>

          <button type="button" onClick={() => removeSkill(index)}>
            削除
          </button>
        </div>
      ))}

      <button type="button" onClick={addSkill}>
        スキルを追加
      </button>

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

動的フォームではuseFieldArrayという特別なフックを使います。

  • fields: 現在のフィールド配列
  • append: フィールドを追加する関数
  • remove: フィールドを削除する関数

これらを使って、ユーザーが必要に応じてフィールドを増減できるフォームが作れますよ。

ファイルアップロード機能を追加してみよう

ファイルアップロードも簡単に実装できます。 画像アップロードフォームを作ってみましょう。

function FileUploadForm() {
  const { register, handleSubmit, formState: { errors }, watch } = useForm();

  const watchedFile = watch('avatar');

  const onSubmit = async (data) => {
    if (data.avatar && data.avatar[0]) {
      const formData = new FormData();
      formData.append('avatar', data.avatar[0]);
      formData.append('name', data.name);

      try {
        const response = await fetch('/api/upload', {
          method: 'POST',
          body: formData,
        });

        if (response.ok) {
          alert('アップロードが完了しました');
        }
      } catch (error) {
        console.error('Upload error:', error);
      }
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label>名前:</label>
        <input
          {...register('name', { required: '名前は必須です' })}
          type="text"
        />
        {errors.name && <span>{errors.name.message}</span>}
      </div>

      <div>
        <label>アバター画像:</label>
        <input
          {...register('avatar', {
            required: 'ファイルを選択してください',
            validate: {
              fileSize: (files) => {
                if (files[0] && files[0].size > 5 * 1024 * 1024) {
                  return 'ファイルサイズは5MB以下にしてください';
                }
                return true;
              },
              fileType: (files) => {
                if (files[0] && !files[0].type.startsWith('image/')) {
                  return '画像ファイルを選択してください';
                }
                return true;
              }
            }
          })}
          type="file"
          accept="image/*"
        />
        {errors.avatar && <span>{errors.avatar.message}</span>}
      </div>

      {watchedFile && watchedFile[0] && (
        <div>
          <p>選択されたファイル: {watchedFile[0].name}</p>
          <p>ファイルサイズ: {(watchedFile[0].size / 1024).toFixed(2)} KB</p>
        </div>
      )}

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

ファイルアップロードでも、サイズや形式のバリデーションができます。

ファイルサイズのチェック

fileSize: (files) => {
  if (files[0] && files[0].size > 5 * 1024 * 1024) {
    return 'ファイルサイズは5MB以下にしてください';
  }
  return true;
}

ファイルサイズを5MBまでに制限しています。

ファイル形式のチェック

fileType: (files) => {
  if (files[0] && !files[0].type.startsWith('image/')) {
    return '画像ファイルを選択してください';
  }
  return true;
}

画像ファイルのみに限定しています。 このように、ファイルアップロードでも細かな制御ができるんです。

パフォーマンス最適化でさらに高速化

React Hook Formの性能を最大限に活用するための最適化テクニックをご紹介します。 より高速で快適なフォームを作りましょう。

UIライブラリとの統合

既存のUIライブラリと組み合わせる場合は、Controllerを使用します。 Material-UIやAnt Designなどとの連携で威力を発揮しますよ。

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

function OptimizedForm() {
  const { control, handleSubmit } = useForm();

  return (
    <form onSubmit={handleSubmit(console.log)}>
      {/* Material-UIのTextFieldと統合 */}
      <Controller
        name="email"
        control={control}
        rules={{
          required: 'メールアドレスは必須です',
          pattern: {
            value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
            message: 'メールアドレスの形式が正しくありません'
          }
        }}
        render={({ field, fieldState: { error } }) => (
          <div>
            <input
              {...field}
              type="email"
              placeholder="メールアドレス"
            />
            {error && <span>{error.message}</span>}
          </div>
        )}
      />

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

Controllerを使うことで、カスタムコンポーネントとも簡単に連携できます。 既存のデザインシステムを活用しながら、React Hook Formの恩恵を受けられますね。

条件付きレンダリングで無駄を削減

特定の条件でのみ再レンダリングを発生させることができます。 ユーザーの選択に応じて、フォームを動的に変更してみましょう。

function ConditionalRenderForm() {
  const { register, handleSubmit, watch } = useForm();

  // 特定のフィールドのみ監視
  const userType = watch('userType');
  const showCompanyField = userType === 'business';

  return (
    <form onSubmit={handleSubmit(console.log)}>
      <select {...register('userType')}>
        <option value="individual">個人</option>
        <option value="business">法人</option>
      </select>

      {/* 条件付きレンダリング */}
      {showCompanyField && (
        <input
          {...register('companyName', {
            required: '会社名は必須です'
          })}
          placeholder="会社名"
        />
      )}

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

userTypebusinessの場合のみ、会社名フィールドが表示されます。 不要なフィールドは表示されないので、パフォーマンスとUXの両方が向上しますよ。

メモ化でさらなる最適化

パフォーマンスが重要な場合は、React.memoを活用しましょう。 コンポーネントの分離と組み合わせることで、効果的に最適化できます。

import React, { memo } from 'react';

const FormField = memo(({ register, error, ...props }) => {
  console.log('FormField rendered'); // 再レンダリングを確認

  return (
    <div>
      <input {...register} {...props} />
      {error && <span>{error.message}</span>}
    </div>
  );
});

function MemoizedForm() {
  const { register, handleSubmit, formState: { errors } } = useForm();

  return (
    <form onSubmit={handleSubmit(console.log)}>
      <FormField
        register={register('name', { required: '名前は必須です' })}
        error={errors.name}
        placeholder="名前"
      />
      
      <FormField
        register={register('email', { required: 'メールアドレスは必須です' })}
        error={errors.email}
        placeholder="メールアドレス"
      />

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

memoを使うことで、不要な再レンダリングを防げます。 大量のフィールドがあるフォームでは、特に効果的ですよ。

エラーハンドリングでユーザビリティ向上

適切なエラーハンドリングで、ユーザーフレンドリーなフォームを作成しましょう。 分かりやすいエラー表示は、ユーザー体験を大きく向上させます。

見やすいエラー表示を作ろう

エラーメッセージを見やすく表示する方法をご紹介します。 まずは基本的なパターンから見てみましょう。

function ErrorHandlingForm() {
  const { register, handleSubmit, formState: { errors }, setError, clearErrors } = useForm();

  const onSubmit = async (data) => {
    try {
      clearErrors(); // 既存のエラーをクリア
      
      const response = await fetch('/api/submit', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data),
      });

      if (!response.ok) {
        const errorData = await response.json();
        
        // サーバーサイドエラーを表示
        if (errorData.errors) {
          Object.entries(errorData.errors).forEach(([field, message]) => {
            setError(field, { message });
          });
        } else {
          setError('root', { message: '送信に失敗しました' });
        }
      }
    } catch (error) {
      setError('root', { message: 'ネットワークエラーが発生しました' });
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {/* 全体的なエラーメッセージ */}
      {errors.root && (
        <div className="error-banner">
          {errors.root.message}
        </div>
      )}

      <div className="form-group">
        <input
          {...register('email', {
            required: 'メールアドレスは必須です',
            pattern: {
              value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
              message: 'メールアドレスの形式が正しくありません'
            }
          })}
          placeholder="メールアドレス"
          className={errors.email ? 'error-input' : ''}
        />
        {errors.email && (
          <span className="error-message">
            {errors.email.message}
          </span>
        )}
      </div>

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

このコードのポイントを見てみましょう。

全体エラーとフィールドエラーの使い分け

// 全体的なエラー(ネットワークエラーなど)
setError('root', { message: 'ネットワークエラーが発生しました' });

// 特定フィールドのエラー
setError('email', { message: 'このメールアドレスは既に登録されています' });

全体に関わるエラーはrootに、特定フィールドのエラーはフィールド名に設定します。

動的なエラー設定

if (errorData.errors) {
  Object.entries(errorData.errors).forEach(([field, message]) => {
    setError(field, { message });
  });
}

サーバーから返されたエラーを動的に設定できます。 APIの応答に応じて、適切なフィールドにエラーを表示できますね。

エラーのスタイリングで見た目を向上

CSSでエラー表示を見やすくしましょう。 視覚的に分かりやすいエラー表示を作ってみます。

.form-group {
  margin-bottom: 1rem;
}

.error-input {
  border: 2px solid #ff4444;
  background-color: #fff5f5;
}

.error-message {
  color: #ff4444;
  font-size: 0.875rem;
  margin-top: 0.25rem;
  display: block;
}

.error-banner {
  background-color: #ff4444;
  color: white;
  padding: 1rem;
  border-radius: 4px;
  margin-bottom: 1rem;
}

エラーのあるフィールドはerror-inputクラスで強調表示されます。 赤い枠線と薄い赤の背景色で、一目でエラーがあることが分かりますね。

エラーメッセージも赤文字で目立つように表示されます。 全体エラーは上部にバナー形式で表示して、重要性を伝えましょう。

まとめ:効率的なフォーム開発を始めよう

React Hook Formを使うことで、複雑なフォーム処理が驚くほど簡単になります。 従来の方法と比べて、開発効率が大幅に向上するんです。

React Hook Formの主なメリット

  • パフォーマンス向上: 最小限の再レンダリングで高速動作
  • 開発効率: シンプルなAPIで少ないコード量
  • 型安全性: TypeScriptとの親和性が高い
  • 柔軟性: 複雑なバリデーションやカスタマイズが可能
  • 軽量: 小さなバンドルサイズでパフォーマンス最適化

学習のステップ

React Hook Formを導入する際は、以下のステップで進めることをおすすめします。

// 1. 最初はシンプルな形から始める
const { register, handleSubmit } = useForm();

// 2. バリデーションを追加
const { register, handleSubmit, formState: { errors } } = useForm();

// 3. さらに高度な機能を活用
const { register, handleSubmit, watch, control, formState } = useForm();

実践的な活用場面

  • 基本フォーム: お問い合わせフォームや簡単な入力フォーム
  • ユーザー登録: 複雑なバリデーションが必要な登録フォーム
  • 動的フォーム: フィールドが動的に変わるアンケートフォーム
  • ファイルアップロード: 画像や文書のアップロード機能

まずは簡単なフォームから始めて、徐々に機能を覚えていくのがおすすめです。

最初は基本的なregisterhandleSubmitから始めてみてください。 慣れてきたら、バリデーションやカスタム機能を追加していきましょう。

React Hook Formは、React開発において非常に強力なツールです。 従来のフォーム実装に比べて、コード量が大幅に削減され、パフォーマンスも向上します。

ぜひ今回紹介した内容を参考にして、実際のプロジェクトで活用してみてください。 きっと開発効率が大幅に向上して、より良いユーザー体験を提供できるはずですよ!

関連記事