React Hook Form完全ガイド|使い方・バリデーション・実践例

この記事では、React Hook Formの概要から基本的な使い方、ZodやMaterial-UIなど外部ライブラリとの連携、watch・setValue等の活用法、実務事例までを網羅的に解説し、効率的で保守性の高いフォーム実装の悩みを解決します。

目次

React Hook Formの概要と特長

react+hook+form

ライブラリの基本概念と役割

React Hook Formは、Reactでのフォーム管理を効率化するための軽量なライブラリです。Reactのカスタムフックを活用し、フォームデータの状態管理やバリデーション、エラーハンドリングを直感的かつ高パフォーマンスに実装できます。
従来のフォーム管理では、複数のuseStateonChangeイベントのハンドリングが必要でしたが、React Hook FormではuseFormというフックを基点に、一元的にフォームの振る舞いを制御できます。

このライブラリの最大の役割は、再描画の最小化によるパフォーマンス向上と、開発者体験(DX)の改善にあります。フォーム項目の多い大規模アプリケーションでも軽快に動作するため、近年多くのプロジェクトで採用が進んでいます。

主な機能とメリット

React Hook Formには、シンプルながら強力な機能が多数搭載されています。代表的なものを以下に挙げます。

  • パフォーマンス重視のアーキテクチャ – 入力ごとの再レンダリングを最小限に抑え、フォームの応答性を高めます。
  • シンプルなAPI設計registerhandleSubmiterrorsなど覚えやすいメソッド群で構成。
  • 柔軟なバリデーション – HTML標準のバリデーション属性やカスタムルール、外部スキーマバリデーションライブラリ(Zod, Yupなど)と容易に統合。
  • 非制御コンポーネントの活用 – Reactの非制御フォーム要素と親和性が高く、レンダリング負荷を低減。
  • TypeScript対応 – 型情報を活用した安全なフォーム設計が可能。

これらにより、React Hook Formは「少ないコードで、扱いやすく、パフォーマンスにも優れたフォーム実装」を実現します。

他のフォーム管理ライブラリとの違い

Reactにおけるフォーム管理ライブラリとしては、FormikやRedux Formなども広く利用されています。しかし、React Hook Formにはそれらと異なる特徴があります。

項目 React Hook Form Formik / Redux Form
状態管理方式 非制御コンポーネント中心 制御コンポーネント中心
レンダリング頻度 必要なフィールドのみ再レンダリング フォーム全体が再レンダリングされがち
コード量 少ない やや多め
外部バリデーション連携 容易(Zod, Yup等) 可能だが設定量が多い場合も

特に「非制御コンポーネント」を前提とした設計により、フォームのパフォーマンス最適化がしやすい点はReact Hook Formの大きな強みです。一方で、フォームの挙動を細かく制御したい場合は、Controllerコンポーネントを併用することで、制御コンポーネント型の実装ともシームレスに対応できます。

React Hook Formの初期設定と導入手順

react+hook+form

インストール方法とセットアップ

React Hook Formは、軽量かつ高速なフォーム状態管理を可能にする人気のライブラリです。導入は非常にシンプルで、わずかなコマンドで環境に追加できます。まずは、プロジェクトにパッケージをインストールしましょう。

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

次に、フォームを作成したいコンポーネントでuseFormをインポートします。基本的なセットアップは以下の通りです。

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

export default function MyForm() {
  const { register, handleSubmit } = useForm();
  const onSubmit = data => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('example')} />
      <button type="submit">送信</button>
    </form>
  );
}

これで、React Hook Formの基本的な動作が利用可能になります。インストールから最初のフォーム動作確認まで、数分で完了できるのが大きな魅力です。

基本的なフォーム作成例

React Hook Formを使えば、フォームの入力値管理とバリデーションを簡潔に実装できます。以下は、氏名とメールアドレスを入力するシンプルなフォーム例です。

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

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

export default function ContactForm() {
  const { register, handleSubmit, formState: { errors } } = useForm<FormValues>();
  const onSubmit = (data: FormValues) => {
    console.log('送信データ:', data);
  };

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

      <div>
        <label>メールアドレス</label>
        <input
          type="email"
          {...register('email', {
            required: 'メールアドレスは必須です',
            pattern: {
              value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
              message: '有効なメールアドレスを入力してください'
            }
          })}
        />
        {errors.email && <p>{errors.email.message}</p>}
      </div>

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

このコードでは、registerを利用して各入力フィールドにReact Hook Formの機能を紐づけています。さらにformState.errorsから各項目のエラーメッセージを取得し、リアルタイムにエラーメッセージを表示できます。

TypeScript環境での設定方法

React Hook FormはTypeScriptとの相性が非常に良く、フォームデータの型安全性を担保できます。型定義を行うことで、入力データの構造が明確になり、予期しないバグを防げます。

TypeScriptで利用する際は、useFormのジェネリクス引数にフォームデータ型を渡します。

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

interface LoginFormInputs {
  username: string;
  password: string;
}

export default function LoginForm() {
  const { register, handleSubmit, formState: { errors } } = useForm<LoginFormInputs>();
  
  const onSubmit = (data: LoginFormInputs) => {
    console.log('ログイン情報:', data);
  };

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

      <input type="password" {...register('password', { required: 'パスワードは必須です' })} />
      {errors.password && <p>{errors.password.message}</p>}

      <button type="submit">ログイン</button>
    </form>
  );
}

この方法を使えば、IDEの補完機能が効きやすくなり、入力フィールド名のタイプミスやデータ型の不一致をコンパイル時に検知できます。TypeScript環境でのReact Hook Form利用は、大規模開発や長期的なプロジェクトで特に有効です。

フォーム入力データの管理方法

react+hook+form

useFormによる状態管理の基本

React Hook Formでは、フォームの状態管理の中心となるフックがuseFormです。このフックを利用することで、入力値やエラー状態、バリデーション結果などを効率的に管理できます。伝統的なフォーム管理ライブラリと異なり、各フォームフィールドを非制御コンポーネントとして扱うため、レンダリング回数を最小限に抑えられるのが大きな特長です。

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

function MyForm() {
  const { register, handleSubmit, formState: { errors } } = useForm();
  
  const onSubmit = (data) => {
    console.log(data);
  };
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("name")} />
      <button type="submit">送信</button>
    </form>
  );
}

registerによってフィールドを紐付けることで、フォームの入力値はReact Hook Formの状態として自動的に管理されます。

getValuesでの値取得方法

getValuesは現在のフォームの入力値を取得するためのメソッドです。フォーム送信以外のタイミングで値を参照したい場合に有効で、例えば入力途中の内容を使ってサマリー表示するケースで活用できます。

const { register, getValues } = useForm();

// 任意のタイミングで値を取得
const currentValues = getValues();

特定フィールドの値だけを取得する場合は、getValues("fieldName")のようにフィールド名を指定します。

watchでのリアルタイム監視

watchはフォーム入力値をリアルタイムで監視するためのメソッドです。監視対象フィールドが更新されるとコンポーネントが再レンダリングされ、即座にUIへ反映できます。

全フィールドの監視

フィールド名を指定せずにwatch()を呼び出すと、全てのフォームフィールドを監視対象にできます。

const allValues = watch();

特定フィールドの条件付き監視

1つまたは複数のフォームフィールド名を指定すれば、必要な項目だけを監視可能です。例えば、住所入力フォームで「都道府県」が変更された時だけ市区町村の選択肢を更新するといった動作が実現できます。

const prefecture = watch("prefecture");

デフォルト値の設定と反映

useFormの引数にdefaultValuesを指定することで、watchによる監視でも初期値が正しく反映されます。これにより、既存データの編集画面でも値が初期表示されます。

const { watch } = useForm({
  defaultValues: { name: "山田太郎" }
});

動的UIの変更への活用例

watchで特定フィールドを監視し、その値に応じて表示コンポーネントを切り替える動的UIが構築できます。例えば、チェックボックスの状態で入力フォームの表示/非表示を制御するなどです。

setValueによるデータ更新

setValueは指定したフィールドの入力値をプログラム的に更新するためのメソッドです。APIから取得した値をフォームに反映したい場合や、ユーザー操作に応じて自動計算した結果を反映したい場合に便利です。

基本的な使い方

setValue("email", "example@example.com");

フィールド名と新しい値を指定すると、その値がフォーム状態に反映されます。

動的な値の設定方法

setValueはユーザー操作やAPIレスポンスに応じて動的に呼び出すことができます。たとえば、郵便番号から住所を自動補完するロジックでの利用が代表例です。

使用時の注意点とベストプラクティス

  • shouldValidate オプションをtrueにすると、値更新後にバリデーションを実行可能
  • shouldDirtyを使うと、変更済み状態として扱うかどうかを制御できる
  • 頻繁な更新を行う場合はパフォーマンスへの影響を考慮する

resetとsetErrorの活用方法

resetはフォーム全体をリセットし、初期状態へ戻すメソッドです。送信後の状態初期化や、編集内容を破棄して再読み込みする際に役立ちます。また、引数に新しいdefaultValuesを渡すことで、リセット時に別の初期値を適用することも可能です。

一方、setErrorは任意のフィールドにエラー状態を手動で設定するためのメソッドです。APIレスポンスから取得したサーバーサイドの検証エラーを表示したい場合などに重宝します。

setError("username", {
  type: "manual",
  message: "このユーザー名は既に使用されています"
});

これらのメソッドを組み合わせて適切に活用することで、React Hook Formを使ったフォーム管理の柔軟性とユーザー体験を大きく向上できます。

バリデーションの実装方法

react+hook+form

React Hook Form標準のバリデーション設定

React Hook Formは、フォームのバリデーションを簡潔かつ柔軟に実装できる標準機能を備えています。入力要件や制約をオブジェクト形式で指定することで、JavaScriptのネイティブバリデーションと同等の効果を得ることができ、追加ライブラリを使用せずとも高品質なフォームチェックが可能です。以下では、よく使われるバリデーション手法を詳しく紹介します。

エラー表示とエラーメッセージの設定

標準バリデーションでは、register関数の第二引数にバリデーションルールを指定し、発生したエラーはformState.errorsから取得します。各フィールドに対してrequiredmaxLengthなどを設定し、messageプロパティでユーザにわかりやすいエラーメッセージを表示することができます。

{`

{errors.email && {errors.email.message}}
`}

複数条件のバリデーション

1つのフィールドに複数のルールを組み合わせることも可能です。例えば「必須入力」「最大文字数」「パターンマッチ」などを同時に定義できます。React Hook Formではこれらをオブジェクトで連続して記述するため、複雑な入力制約でもシンプルに実装できます。

{`

`}

バリデーション実行のタイミング設定(mode / reValidateMode)

React Hook Formでは、useFormのオプションmodereValidateModeでバリデーション実行のタイミングを制御できます。

  • onSubmit:送信時にのみ検証(デフォルト)
  • onChange:入力変更の度に検証
  • onBlur:フォーカスが外れたタイミングで検証
  • all:全てのイベントで検証

これによりUXに応じたバリデーション挙動を実現できます。例えば入力途中ではエラーを隠しておき、送信時にまとめて表示したい場合はonSubmitを選択すると良いでしょう。

手動バリデーションの実行方法

標準のバリデーションに加え、必要に応じてプログラム的にチェックを走らせたい場合はtriggerメソッドを使用します。trigger()は単一フィールド、または複数フィールドを指定して検証を行うことができます。

{`
// 単一フィールドの手動検証
await trigger("email");

// 全フィールドの手動検証
await trigger();
`}

この手法は、ステップ形式のフォームや条件付きの入力チェックなど、タイミングを細かく制御したい場面で特に有効です。

カスタムバリデーションの作成

React Hook Formではvalidateプロパティを利用して独自のバリデーションロジックを組み込むことができます。関数として実装し、trueを返せば成功、文字列を返せばその文字列がエラーメッセージとして表示されます。

{`
 value.includes("@") || "パスワードに@を含めてください"
})} />
`}

この方法を使えば、外部APIとの照合や、他フィールドの値との比較など、高度なチェックロジックを簡単に導入できます。

フォーム全体や複数項目に対するバリデーション

フォーム全体や複数項目にまたがるバリデーションには、handleSubmit内やresolverを活用すると便利です。例えば「パスワード」と「パスワード(確認)」の一致確認や、郵便番号と住所入力の整合性チェックなど、関連する複数フィールドの値を取得して一括で検証できます。

{`
const onSubmit = data => {
  if (data.password !== data.passwordConfirm) {
    setError("passwordConfirm", { type: "manual", message: "パスワードが一致しません" });
  } else {
    // 送信処理
  }
};
`}

このようにReact Hook Formは単一フィールドだけでなく、フォーム全体のルールを包括的に管理できるため、複雑なバリデーション要件にも柔軟に対応できます。

外部バリデーションライブラリとの連携

react+hook+form

Zodとの統合方法

インストールと基本設定

ZodはTypeScriptに特化したスキーマベースのバリデーションライブラリで、React Hook Formと組み合わせることで型安全かつ堅牢なフォームバリデーションを実現できます。まずは必要なパッケージをプロジェクトにインストールします。

npm install zod @hookform/resolvers
# または
yarn add zod @hookform/resolvers

インストール後、@hookform/resolvers/zodから提供されるzodResolveruseFormresolverオプションに指定することで、Zodスキーマを直接React Hook Formに組み込むことができます。

import { useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";

const schema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
});

const { register, handleSubmit, formState: { errors } } = useForm({
  resolver: zodResolver(schema)
});

スキーマ定義とバリデーション例

ZodではJavaScript/TypeScriptのオブジェクト構造に基づいてバリデーションルールを宣言的に定義できます。例えば、メールアドレスやパスワードの制約は以下の通り非常に簡潔に記述可能です。

const loginSchema = z.object({
  email: z.string()
    .min(1, "メールアドレスは必須です")
    .email("メールアドレスの形式が正しくありません"),
  password: z.string()
    .min(8, "パスワードは8文字以上で入力してください")
});

このようにスキーマに直接エラーメッセージを指定することで、後述の日本語対応もスムーズに行えます。

refineを使った高度なチェック(例:パスワード一致)

Zodのrefineメソッドを活用することで、複数フィールド間の関連性を評価する高度なバリデーションが可能です。例えばパスワードと確認用パスワードの一致チェックは以下のように記述できます。

const passwordSchema = z.object({
  password: z.string().min(8, "パスワードは8文字以上で入力してください"),
  confirmPassword: z.string().min(8, "確認用パスワードも8文字以上必要です")
}).refine((data) => data.password === data.confirmPassword, {
  message: "パスワードと確認用パスワードが一致しません",
  path: ["confirmPassword"]
});

この実装により、ユーザー入力の整合性を高め、後続の処理でエラーを防ぐことができます。

条件付きバリデーションの実装

条件付きバリデーションでは、特定のフィールドの値に応じて別フィールドのバリデーションを変化させます。Zodの場合、refinesuperRefineを利用します。

const profileSchema = z.object({
  hasWebsite: z.boolean(),
  websiteUrl: z.string().url().optional()
}).superRefine((data, ctx) => {
  if (data.hasWebsite && !data.websiteUrl) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "WebサイトURLを入力してください",
      path: ["websiteUrl"]
    });
  }
});

このアプローチにより、ユーザー体験を損なわず必要な情報を適切に取得できます。

日本語エラーメッセージ対応

Zodは標準で英語メッセージを生成しますが、スキーマ定義時に直接日本語メッセージを埋め込むことで、日本語対応が容易にできます。また、共通メッセージ管理用の定数を用意して再利用性を高めるのもおすすめです。

const messages = {
  required: "この項目は必須です",
  invalidEmail: "正しいメールアドレスを入力してください"
};

const contactSchema = z.object({
  email: z.string().min(1, messages.required).email(messages.invalidEmail),
  name: z.string().min(1, messages.required)
});

このようにすることで、アプリ全体で統一感のある日本語バリデーションメッセージを提供できます。

Yupとの統合方法と特徴

YupはJavaScriptオブジェクトのスキーマ構築とバリデーションに広く利用されるライブラリで、React Hook Formと組み合わせる場合も@hookform/resolversを介して簡単に統合できます。Zodと異なり、Yupはチェーンメソッドによる記述が中心で、比較的柔軟な型定義が可能です。

import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";

const schema = yup.object().shape({
  email: yup.string().email("メールアドレスの形式が正しくありません").required("必須項目です"),
  password: yup.string().min(8, "8文字以上必要です").required("必須項目です")
});

const { register, handleSubmit } = useForm({
  resolver: yupResolver(schema)
});

Yupは豊富なビルトインバリデーションや条件分岐、非同期チェックに強く、既存のJavaScriptプロジェクトにスムーズに導入しやすいという利点があります。一方で、TypeScriptとの統合性や型推論の自然さではZodにやや劣る場合があり、プロジェクト要件に応じて選択すると良いでしょう。

React Hook FormとUIコンポーネントライブラリの併用

react+hook+form

Material-UIとの統合方法

インストールとセットアップ

Material-UI(現 MUI)は、洗練されたUIコンポーネントを提供するReact向けライブラリで、React Hook Formと組み合わせることで、見た目と操作性の両方に優れたフォームを効率的に構築できます。まずは必要なパッケージをインストールします。

npm install @mui/material @emotion/react @emotion/styled react-hook-form

インストール後、必要に応じてテーマを設定します。Material-UIのThemeProviderを使うことで、カラーパレットやタイポグラフィを一括で管理できます。React Hook FormのuseFormを使ってフォームの状態管理を行い、Material-UIのTextFieldCheckboxなどと連携させるのが基本の流れです。

{`import { useForm, Controller } from "react-hook-form";
import { TextField, Button } from "@mui/material";

export default function MyForm() {
  const { control, handleSubmit } = useForm();
  const onSubmit = data => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Controller
        name="firstName"
        control={control}
        render={({ field }) =>
          <TextField {...field} label="First Name" variant="outlined" />
        }
      />
      <Button type="submit" variant="contained">送信</Button>
    </form>
  );
}`}

複雑なフォーム構築例

実務では、単一の入力フィールドだけでなく、複数の入力形式(テキスト、セレクト、チェックボックス、日付ピッカーなど)を組み合わせたフォームが求められます。React Hook FormとMaterial-UIを組み合わせる場合、Controllerコンポーネントを用いてフォームコンポーネントをラップし、値やバリデーションをReact Hook Formに委譲します。

例えばユーザー登録フォームでは以下のような構成が可能です。

  • TextField:ユーザー名やメールアドレスの入力
  • Select:国やカテゴリの選択
  • Checkbox:利用規約の同意
  • DatePicker:生年月日や予約日などの日付選択

複合フォームを構築する際は、全てのコンポーネントでReact Hook Formが管理するcontrolを共有し、フォーム送信時に一括でデータを取得・バリデーションすることで、コードの統一性と保守性が向上します。

カスタムバリデーションと状態管理

Material-UIの入力コンポーネントは、標準的なHTMLフォーム要素と異なり内部的に状態を保持していることがあります。そのため、React Hook Formによるバリデーションと連動させるには、Controllerrulesプロパティやvalidate関数を活用します。

{`<Controller
  name="email"
  control={control}
  rules={{
    required: "メールアドレスは必須です",
    pattern: {
      value: /^[\\w.%+-]+@[\\w.-]+\\.[A-Za-z]{2,}$/i,
      message: "正しいメールアドレスを入力してください"
    }
  }}
  render={({ field, fieldState: { error } }) =>
    <TextField
      {...field}
      label="Email"
      error={!!error}
      helperText={error ? error.message : ""}
    />
  }
/>`}

また、依存するフィールド間の状態管理(例:セレクトボックスの選択内容に応じた入力欄の表示切替)も容易です。React Hook FormのwatchとMaterial-UIのコンディショナルレンダリングを組み合わせることで、動的かつユーザーフレンドリーなフォームを実現できます。

このように、React Hook FormとMaterial-UIを併用することで、高い開発効率と優れたユーザー体験の両立が可能になります。

NextUIとの統合方法

Input, Textarea, Radioの実装

NextUIはモダンで軽量なUIコンポーネントを提供するライブラリで、レスポンシブデザインやカスタマイズ性に優れています。React Hook Formと組み合わせれば、スタイリッシュかつバリデーション対応のフォームを簡単に作成できます。以下は基本的な実装例です。

{`import { useForm, Controller } from "react-hook-form";
import { Input, Textarea, Radio } from "@nextui-org/react";

export default function ContactForm() {
  const { control, handleSubmit } = useForm();
  const onSubmit = (data) => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Controller
        name="name"
        control={control}
        render={({ field }) =>
          <Input {...field} label="お名前" fullWidth bordered />
        }
      />
      <Controller
        name="message"
        control={control}
        render={({ field }) =>
          <Textarea {...field} label="メッセージ" fullWidth bordered />
        }
      />
      <Controller
        name="gender"
        control={control}
        render={({ field }) =>
          <Radio.Group {...field} label="性別">
            <Radio value="male">男性</Radio>
            <Radio value="female">女性</Radio>
          </Radio.Group>
        }
      />
      <button type="submit">送信</button>
    </form>
  );
}`}

イベント仕様の注意点

NextUIのコンポーネントは、HTML標準のonChangevalueとは若干異なるイベント仕様を持つ場合があります。特にRadio.GroupSelectなどは、イベントオブジェクトではなく選択値を直接返すことがあるため、Controller内でonChangeを適切にマッピングする必要があります。

{`render={({ field: { onChange, value } }) => (
  <Radio.Group value={value} onChange={(val) => onChange(val)}>
    <Radio value="A">A</Radio>
    <Radio value="B">B</Radio>
  </Radio.Group>
)}`}

この仕様を理解していないと、React Hook Formに値が正常に渡らず、バリデーションやフォーム送信時のデータ取得ができないケースがあります。そのため、NextUIの各コンポーネントのProps仕様や返却するイベント値の型を事前に確認し、React Hook Formに適切に連携させることが重要です。

Draft.jsとの組み合わせ例

Draft.jsはFacebookが開発したリッチテキストエディタライブラリで、記事作成やコメント入力など高度なテキスト入力機能を求められるケースで活用されます。React Hook Formと組み合わせる場合、Controllerを介してエディタの状態(EditorState)をフォームデータとして管理します。

{`import { useForm, Controller } from "react-hook-form";
import { Editor, EditorState } from "draft-js";
import "draft-js/dist/Draft.css";
import { useState } from "react";

export default function DraftForm() {
  const { control, handleSubmit } = useForm();
  const onSubmit = data => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Controller
        name="content"
        control={control}
        defaultValue={EditorState.createEmpty()}
        render={({ field: { onChange, value } }) => (
          <Editor editorState={value} onChange={onChange} />
        )}
      />
      <button type="submit">送信</button>
    </form>
  );
}`}

Draft.jsではHTMLではなくエディタ固有のステートオブジェクトを扱うため、送信前にconvertToRawやHTML変換関数を用いてサーバーで扱いやすい形式に変換すると良いでしょう。

shadcn/uiコンポーネントとの連携方法

shadcn/uiはTailwind CSSをベースにした美しいUIコンポーネント集で、React Hook Formと非常に相性が良いです。カスタマイズ性が高く、デザインシステムに合わせたフォームUIを短時間で構築できます。

{`import { useForm, Controller } from "react-hook-form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";

export default function ShadcnForm() {
  const { control, handleSubmit } = useForm();
  const onSubmit = data => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
      <Controller
        name="username"
        control={control}
        render={({ field }) =>
          <Input {...field} placeholder="ユーザー名" />
        }
      />
      <Button type="submit">送信</Button>
    </form>
  );
}`}

Tailwind CSSのユーティリティクラスを併用することで、余白や配色、フォーカス時のスタイルなどを柔軟に調整可能です。さらにshadcn/uiのコンポーネントもControllerで包むことにより、React Hook Formによるバリデーションや状態管理とスムーズに連携できます。

Controllerコンポーネントの使い方

react+hook+form

基本的な役割と使い所

React Hook FormのControllerコンポーネントは、非制御コンポーネントでは直接対応できないUIライブラリやカスタムコンポーネントとフォームバリデーション機能を橋渡しするための役割を担います。通常、React Hook Formはregisterを使ってフォーム要素を直接登録しますが、外部UIコンポーネント(例:Material-UIやNextUI)のようにrefを直接渡せない場合、代わりにControllerを使用します。

主な利用シーンは以下の通りです。

  • サードパーティ製のInputコンポーネントをReact Hook Formに組み込みたいとき
  • valueとonChangeイベントをカスタマイズして制御する必要がある場合
  • 表示形式や内部値が異なるデータの同期が必要な場合(例:日付ピッカーやリッチテキストエディタ)

バリデーションを含む利用例

Controllerは直接rulesプロパティを設定することで、React Hook Form標準のバリデーションを適用できます。例えば、必須入力チェックや文字数制限などを簡潔に定義できます。

{`import { useForm, Controller } from "react-hook-form";
import TextField from "@mui/material/TextField";

function MyForm() {
  const { control, handleSubmit } = useForm();
  const onSubmit = data => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Controller
        name="username"
        control={control}
        rules={{ 
          required: "ユーザー名は必須です", 
          minLength: { value: 3, message: "3文字以上入力してください" }
        }}
        render={({ field, fieldState: { error } }) => (
          <TextField
            {...field}
            label="ユーザー名"
            error={!!error}
            helperText={error ? error.message : ""}
          />
        )}
      />
      <button type="submit">送信</button>
    </form>
  );
}`}

このように、UIコンポーネントの表示とバリデーションメッセージの出力が密接に連動するため、ユーザーにとっても直感的な入力環境を提供できます。

非制御コンポーネントとの違い

React Hook Formは非制御コンポーネントを推奨しますが、Controllerを使う場合は制御コンポーネントとしてのデータ同期が必要になります。非制御コンポーネントは内部でDOMの状態を保持し、ref経由でReact Hook Formが直接アクセスできます。一方、ControllervalueonChangeを明示的に渡し、Reactの単方向データフローを維持しながら動作します。

要するに、非制御コンポーネントは軽量でパフォーマンスに優れる一方、Controllerは柔軟性と外部コンポーネント対応を実現します。用途によって使い分けることで、開発効率とパフォーマンスを両立できます。

他メソッドとの併用方法

ControllersetValuewatchなどのReact Hook Formの他メソッドと組み合わせて利用できます。例えば、ユーザーが値を選択したら他のフォームフィールドを自動補完する、変更があれば即座にリアルタイムプレビューを更新する、といった実装が可能です。

{` (
     {
        field.onChange(e);
        setValue("isAdult", e.target.value >= 18);
      }}
    />
  )}
/>`}

また、resetと組み合わせることでフォーム全体を初期化したり、triggerで特定フィールドのバリデーションのみ実行することも可能です。これにより、Controllerを使うケースでもReact Hook Formの豊富なメソッド群を最大限活用できます。

実務で役立つ高度な使い方と事例

react+hook+form

複数フィールドを組み合わせたバリデーション

実務では、単一項目のバリデーションだけでなく、複数の入力値を組み合わせたチェックが必要になるケースが多くあります。たとえば「開始日」と「終了日」の期間整合性や、「パスワード」と「パスワード(確認)」の一致判定などです。React Hook Formでは、watchgetValuesを活用することで、複数フィールドにまたがる検証ロジックを柔軟に実装できます。

const { register, watch, formState: { errors } } = useForm();
const password = watch("password");

<input type="password" {...register("password")} />
<input
  type="password"
  {...register("confirmPassword", {
    validate: value => value === password || "パスワードが一致しません"
  })}
/>
{errors.confirmPassword && <span>{errors.confirmPassword.message}</span>}

このように、他項目の値を参照しながらバリデーションを行うことで、ユーザーにとっても直感的で使いやすい入力フォームを構築できます。

入力値の変換処理(例:郵便番号結合)

フォーム入力の中には、ユーザーの利便性を高めるために複数項目を自動的に結合して処理したいケースがあります。代表例として「郵便番号(前半)」「郵便番号(後半)」の2フィールドを結合し、APIリクエスト時に1つの文字列にまとめるケースがあります。

const { register, getValues, handleSubmit } = useForm();

const onSubmit = () => {
  const { zip1, zip2 } = getValues();
  const zipCode = `${zip1}-${zip2}`;
  console.log(zipCode); // API送信用の結合済み郵便番号
};

この方法により、ユーザーは視覚的に入力しやすく、開発側は必要なデータ形式で扱いやすくなります。バリデーションと合わせて実装することで、入力ミスを減らす効果もあります。

複数項目に同時エラーを設定

申請フォームなどでは、1つのロジックが複数入力項目に影響を与えることがあります。このような場合、setErrorを使うことで同時に複数項目へエラーメッセージを設定できます。

const { setError } = useForm();

setError("startDate", { type: "manual", message: "終了日は開始日以降に設定してください" });
setError("endDate", { type: "manual", message: "終了日は開始日以降に設定してください" });

こうすることで、ユーザーはどの入力が修正対象かを明確に把握でき、エラー解消までのUXを改善できます。業務システムでは特に有効なテクニックです。

バリデーションをテストする方法

精度の高いフォーム実装には、自動テストによるバリデーションチェックが欠かせません。React Hook Formを用いたバリデーションは、JestやReact Testing Libraryと組み合わせてテストできます。

import { render, screen, fireEvent } from "@testing-library/react";
import Form from "./Form";

test("パスワード不一致時にエラーメッセージを表示する", () => {
  render(<Form />);
  fireEvent.input(screen.getByLabelText("パスワード"), { target: { value: "abc" } });
  fireEvent.input(screen.getByLabelText("パスワード(確認)"), { target: { value: "xyz" } });
  fireEvent.submit(screen.getByRole("form"));
  expect(screen.getByText("パスワードが一致しません")).toBeInTheDocument();
});

このように、React Hook Formのバリデーションロジックを自動テストで検証しておくことで、将来的な仕様変更やリファクタリング時にも安心して開発を進められます。

フォーム送信とデータ処理

react+hook+form

onSubmitの基本と非同期処理対応

React Hook Formでは、フォーム送信処理のエントリーポイントとしてhandleSubmit関数が用意されており、この内部でonSubmitコールバックを指定します。onSubmitは、フォームのバリデーションが全て成功した場合にのみ呼び出され、送信データが第一引数として渡されます。これにより、クライアントサイドでの入力チェックとデータ送信がシンプルに連携できます。

非同期処理にも対応しており、onSubmitasync関数として記述することで、API通信やデータベース操作などの待機処理を直感的に実装可能です。例えば、以下のように記述します。

const onSubmit = async (data) => {
  try {
    const response = await fetch('/api/submit', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });
    if (!response.ok) throw new Error('送信に失敗しました');
    alert('送信が完了しました');
  } catch (error) {
    console.error(error);
  }
};

<form onSubmit={handleSubmit(onSubmit)}>
  <input {...register('name')} />
  <button type="submit">送信</button>
</form>

上記のように、非同期処理とReact Hook Formのバリデーションが統合されることで、実務的な堅牢性と保守性が高まります。また、try...catchやエラーハンドリングを適切に組み込むことで、ユーザー体験の向上にもつながります。

サーバーサイドアクションとの統合(Next.js 13対応例)

Next.js 13では、appディレクトリ構造とサーバーサイドアクションが導入され、フォーム送信を直接サーバーサイドで処理する設計が可能になりました。React Hook Formと統合する場合、ブラウザから直接JavaScript Fetch APIでサーバーアクションを呼び出すことで、よりセキュアかつシンプルな実装が可能です。

// app/actions/formActions.js
'use server'

export async function submitFormAction(formData) {
  // ここでDB保存や外部API連携を行う
  return { success: true };
}

// クライアント側
import { submitFormAction } from '@/app/actions/formActions';

const onSubmit = async (data) => {
  const result = await submitFormAction(data);
  if (result.success) {
    alert('送信完了');
  }
};

この方法では、クライアントコードに機密情報を含めずにフォーム送信ロジックをサーバーに集約できます。また、Next.jsのserverモジュールではNode.jsの機能を直接利用できるため、外部APIの認証やデータベース操作を安全に処理できます。

送信後の状態管理とリセット

フォーム送信後の処理として重要なのが、送信ステータスの管理とフォームリセットです。React Hook Formではreset()メソッドを使用することで、すべての入力フィールドを初期状態に戻せます。これにより、送信完了後すぐに新しい入力を受け付ける状態を作れます。

const { handleSubmit, reset } = useForm();

const onSubmit = async (data) => {
  await fetch('/api/send', { method: 'POST', body: JSON.stringify(data) });
  reset(); // フォームを初期化
};

さらに、送信中のローディング状態をuseStateformState.isSubmittingで管理すれば、二重送信防止や送信中インジケーターの表示が簡単に実装可能です。これにより、ユーザーの誤操作を防ぎつつ、直感的なUIを提供できます。

適切な送信後処理は、業務アプリやECサイトなど、ユーザーが繰り返し操作するシナリオで特に効果を発揮します。React Hook Formが提供するシンプルなAPIを活用すれば、開発効率とユーザー体験の両立が実現できます。

開発効率を高めるためのTips

react+hook+form

デバッグとエラー解析のコツ

React Hook Formは軽量かつ高速なフォーム管理が可能ですが、複雑なバリデーションや複数フィールドの依存関係が増えると、デバッグが難しくなることがあります。開発効率を高めるためには、エラーの発生箇所や状態を迅速に特定できる環境を整えることが重要です。

  • devtoolsの活用: React Hook Form DevTools を導入することで、フォームの状態やエラー状況をリアルタイムで可視化できます。
  • コンソール出力による確認: watchgetValues を使って現在の入力値やバリデーション結果をログ出力し、問題の原因を段階的に絞り込みます。
  • エラーオブジェクトの解析: formState.errors を直接確認し、特定フィールドのエラーメッセージやtypeを明確に把握します。
  • 再現性の確保: 問題が発生した際は、同じデータセットや入力手順で再現できる状態を保つことで、修正効率を向上させます。

再利用可能なフォームコンポーネントの設計

複数の画面やプロジェクトで使い回せるフォームコンポーネントを設計することで、開発コストを削減し品質も向上します。React Hook Formは非制御コンポーネントをベースに動作するため、柔軟なカスタムコンポーネント設計がしやすいのが特徴です。

  • 共通UIの切り出し: 入力フィールド、ラベル、エラーメッセージ表示部分を1つのコンポーネントとして分離し、Controllerregisterを内部で扱う形にする。
  • Propsによる汎用性確保: ラベル文言、バリデーションルール、デフォルト値などを外部から渡せるようにする。
  • UIライブラリとの統合: Material-UIやNextUIなどと連携する場合は、Controllerでラップしつつ、フォーム専用のスタイルと機能をセットにする。
  • Storybookでの検証: 再利用コンポーネントをStorybookで管理し、バリエーションやエラーパターンを容易に確認できる体制を作る。

実務でのベストプラクティス集

React Hook Formを実務で活用する際には、単なるAPI理解だけでなく、メンテナンス性やチーム開発の観点も意識することが重要です。以下に、現場で有効なベストプラクティスを整理します。

  1. バリデーションはスキーマ化: YupやZodなどのスキーマバリデーションライブラリと組み合わせて、フォーム要件をコード内で明確化します。
  2. 型安全の確保: TypeScriptと併用し、フォームデータの型定義を導入することで、予期せぬバグを防止します。
  3. 状態の分離: フォームの状態とアプリケーション全体の状態を混在させず、useFormのスコープ内に必要な状態を閉じ込める。
  4. 送信時のUX向上: 送信中はボタンを無効化し、ローディングインジケータを表示するなど、ユーザーが状態を認識しやすい設計にする。
  5. 小さく始めて拡張: 初期実装は最小限に留め、後からフォーム項目やバリデーションを段階的に追加していく。

これらのTipsを意識して実装することで、React Hook Formを使ったフォーム開発の効率と信頼性を大幅に向上させることができます。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です