---
title: "React Hook Formとは？使い方とv7の書き方・検証タイミング【7.91対応】"
url: "https://www.issoh.co.jp/tech/details/2898/"
published: 2024-07-02
updated: 2026-10-27
categories: ["React"]
publisher: "株式会社一創"
---

# React Hook Formとは？使い方とv7の書き方・検証タイミング【7.91対応】

React Hook Form（リアクトフックフォーム）は、Reactでフォームの入力・検証・送信を、余分な再描画を抑えて書くためのライブラリです。標準の入力欄を非制御（uncontrolled）で扱い、入力値やフォーム状態の購読範囲を絞ることで、入力のたびにフォーム全体が再描画されるのを抑えられます。

2026年10月27日時点の最新安定版は[GitHubのリリース一覧](https://github.com/react-hook-form/react-hook-form/releases)でLatestの7.91.0で、v8は8.0.0-beta.5がプレリリースとして出ている段階です。この記事はv7の書き方で、インストール、`register`と`handleSubmit`、検証が走るタイミング、Zod連携、`watch`・`useWatch`・`subscribe`の使い分けまでを、手元で動かした結果を添えて説明します。Hooks自体の基礎は[Reactフック（Hooks）の一覧と使い分け](/tech/details/3671/)で確認できます。

## まとめ：React Hook Formの要点

- **何をするライブラリか**：フォームの値の保持・検証・送信をまとめるReact用ライブラリ。依存パッケージは0。
- **導入**：`npm install react-hook-form`で入るのはv7（7.91.0）。v8はベータで、`@beta`タグは古い8.0.0-beta.0を指す。
- **基本形**：`useForm()`から`register`・`handleSubmit`・`formState`を取り出し、入力欄に`{...register("name")}`を展開する。v6の`ref={register}`は動かない。
- **検証のタイミング**：既定は送信時に初めて検証し（`mode: "onSubmit"`）、送信後は入力のたびに再検証する（`reValidateMode: "onChange"`）。
- **値の監視**：表示を切り替えるなら`useWatch`、保存などの副作用だけなら`subscribe`。`watch`は親ごと再描画され、React Compilerとも相性が悪い。
- **向かない場面**：入力欄が1〜2個ならuseStateで足りる。React 19のform actionだけで完結する送信にも、まず不要。

## React Hook Formの仕組みと現在の版

### 非制御入力と購読による再描画の抑制

フォームの親コンポーネントで各項目の値をuseStateに保持すると、入力のたびにその親が再描画されます。状態を入力欄の子コンポーネントに置けば、再描画の範囲を狭められます。一方、React Hook Formは`register`が返す`ref`で入力欄のDOMを直接参照し、入力値はライブラリ内部でも保持します。入力値やエラーなどの購読範囲を絞れば再描画を抑えられますが、親の再描画に伴って子も再描画される場合があります。[公式サイト](https://react-hook-form.com/)は依存パッケージを持たない点も特徴に挙げています。

### v7.91.0とv8ベータの状況

npmの`latest`は7.91.0です。v7は2021年4月の7.0.0から5年以上続いており、2026年3月の7.72.0から10月の7.91.0までの7か月でもマイナー版が20回出ています。v8は2025年12月に8.0.0-beta.0へ上がり、2026年10月時点の最新プレリリースは8.0.0-beta.5です。正式版の公開日は公式に示されていません。

注意したいのはインストール方法です。[公式のv8移行ガイド](https://react-hook-form.com/migrate-v7-to-v8)には`npm install react-hook-form@beta`と書かれていますが、2026年10月27日時点でnpmの`beta`タグは8.0.0-beta.0（2025年12月公開）を指したままで、最新のbeta.5は入りません。v8を試すなら`react-hook-form@8.0.0-beta.5`と版を指定してください。

v8で書き方が変わる主な点は次のとおりです。v7のコードをそのまま上げると動かなくなる箇所があります。

| 項目                      | v7（現行）     | v8ベータ            |
| ----------------------- | ---------- | ---------------- |
| useFieldArrayの描画用キー     | `field.id` | `field.key`      |
| useFieldArrayの`keyName` | 指定できる      | 削除               |
| `watch(callback)`       | 使える        | 削除（`subscribe`へ） |
| 公開型の`any`               | `any`      | `unknown`        |
| Watchコンポーネントの`names`    | `names`    | `name`           |

型定義でも確認できます。7.91.0の`FieldArrayWithId`はキー名の既定が`'id'`、8.0.0-beta.5では`'key'`です。業務のコードはv7で書き、v8は正式版が出てから[8.0.0-beta.1のリリースノート](https://github.com/react-hook-form/react-hook-form/releases/tag/v8.0.0-beta.1)に並ぶ破壊的変更を確認して移るのが安全です。

## インストールと最小フォームの作成

### npm・yarn・pnpm別のインストールコマンド

peerDependenciesは`react`の`^16.8.0 || ^17 || ^18 || ^19`で、Hooksが使えるReactなら動きます。

```
npm install react-hook-form
# yarn
yarn add react-hook-form
# pnpm
pnpm add react-hook-form
```

### useFormとregisterによる最小フォームの実装

`useForm()`が返す`register("フィールド名", ルール)`を入力欄にスプレッドし、`<form>`の`onSubmit`には`handleSubmit(onSubmit)`を渡します。`onSubmit`は検証を通ったときだけ、入力値をまとめたオブジェクトを受け取ります。エラーは`formState.errors`から取り出します。

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

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

export function ContactForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<Inputs>();

  const onSubmit = (data: Inputs) => console.log(data);

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

      <input
        {...register("email", {
          required: "メールアドレスは必須です",
          pattern: {
            value: /^[^@\s]+@[^@\s]+\.[^@\s]+$/,
            message: "メール形式が正しくありません",
          },
        })}
      />
      {errors.email && <span>{errors.email.message}</span>}

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

`register`の第2引数には`required`・`min`・`max`・`minLength`・`maxLength`・`pattern`・`validate`を書けます。値をオブジェクトにすると、`message`がそのまま`errors.フィールド名.message`に入ります。

## バリデーションが走るタイミングと数値の扱い

### modeとreValidateModeの既定値

「入力した瞬間にエラーが出ない」という戸惑いの多くは既定値の問題です。[useFormのAPIリファレンス](https://react-hook-form.com/docs/useform)では、送信前の検証方法`mode`の既定が`onSubmit`、送信後の再検証`reValidateMode`の既定が`onChange`です。

上の`ContactForm`（nameだけ必須にした版）をjsdom上で動かすと、空のまま入力欄からフォーカスを外してもエラーは出ず、送信して初めて「名前は必須です」が表示されました。その後に1文字入力するとエラーは消えます。送信前から検証したいときは`mode`を変えます。

| mode         | 初回の検証          | 向く画面          |
| ------------ | -------------- | ------------- |
| onSubmit（既定） | 送信時            | 短いフォーム        |
| onBlur       | フォーカスが外れたとき    | 入力途中で責めたくない画面 |
| onTouched    | 最初のblur以降は入力ごと | 会員登録など        |
| onChange     | 入力ごと           | 即時の形式チェック     |
| all          | blurと入力の両方     | 厳密な入力画面       |

```
const { register } = useForm<Inputs>({
  mode: "onTouched",        // 一度触った項目から入力ごとに検証
  reValidateMode: "onChange",
});
```

`onChange`と`all`は入力のたびに検証と再描画が走るので、項目が多いフォームでは描画が重くなります。迷ったら`onTouched`が無難です。

### type=”number”の値の型と数値変換

`<input type="number">`に`register("age")`だけを付けると、20と入力しても`onSubmit`には文字列の`"20"`が届きます（実測）。数値で受け取るには`valueAsNumber: true`を付けます。この場合、空欄は`NaN`になります。

```
<input
  type="number"
  {...register("age", {
    valueAsNumber: true,
    min: { value: 0, message: "0以上で入力してください" },
  })}
/>
```

## ZodとzodResolverによるスキーマ検証

項目が増えてルールが散らばってきたら、スキーマライブラリに検証を集めます。Zodなら`@hookform/resolvers`の`zodResolver`を`useForm`の`resolver`に渡し、型も`z.infer`でスキーマから作ります。

### @hookform/resolvers 6.0.0のインストールエラー

2026年10月19日に出た@hookform/resolvers 6.0.0は、素の`npm install react-hook-form zod @hookform/resolvers`が`ERESOLVE`で失敗します。2026年10月27日にnpm 11.17.0の空のプロジェクトで実行しても再現しました。原因は任意のpeer依存`effect`のバージョン衝突で、[resolversのIssue #882](https://github.com/react-hook-form/resolvers/issues/882)で報告され、[修正PR #883](https://github.com/react-hook-form/resolvers/pull/883)は10月24日（UTC）にマージ済みですが、10月27日時点でnpmの`latest`は6.0.0のままです。修正版が出るまでは5系を指定するのが手早い回避策です。

```
npm install zod @hookform/resolvers@5
```

### z.coerce.numberによる空欄の0変換

数値項目でよく見かける`z.coerce.number().min(0)`は、空欄の`""`を`0`に変換してしまいます。zod 4.6.7で`{ age: "" }`を検証すると結果は`{ age: 0 }`で、未入力のまま送信が通りました。必須の数値なら、`valueAsNumber`で`NaN`にして`z.number()`で弾く組み合わせが確実です。

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

const schema = z.object({
  name: z.string().min(1, "名前は必須です"),
  age: z
    .number({ error: "年齢を数値で入力してください" })
    .min(0, "0以上で入力してください"),
});
type FormValues = z.infer<typeof schema>;

export function ProfileForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<FormValues>({ resolver: zodResolver(schema) });

  return (
    <form onSubmit={handleSubmit((data) => console.log(data.age + 1))}>
      <input {...register("name")} />
      {errors.name && <span>{errors.name.message}</span>}

      <input type="number" {...register("age", { valueAsNumber: true })} />
      {errors.age && <span>{errors.age.message}</span>}

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

このフォームで年齢を空欄のまま送ると「年齢を数値で入力してください」、-1なら「0以上で入力してください」が出て、20なら`{ name: "山田", age: 20 }`が数値のまま届きました。refineで2項目を比べる方法やZod 4の注意点は[ReactのZodフォームバリデーション実装](/tech/details/2371/)、Valibotなど他のスキーマも同じ形で受ける仕組みは[Standard Schemaの解説](/tech/details/11777/)で扱っています。

## watch・useWatch・subscribeの使い分け

ここでは入力値の変更を購読する`watch`・`useWatch`・`subscribe`を、再描画の範囲で比較します。React 19.3.1・React Hook Form 7.91.0・jsdomの検証環境で、初期描画を除き、入力値を3回変更したときの追加の再描画回数を数えました。

| 方法               | useFormを呼ぶ親 | 使う場面        |
| ---------------- | ----------- | ----------- |
| `watch("name")`  | 3回再描画       | 小さなフォームで手早く |
| `useWatch`（子で呼ぶ） | 0回（子が3回）    | 値に応じた表示切り替え |
| `subscribe`      | 0回          | 下書き保存・ログ送信  |

`subscribe`は[7.55.0で追加された](https://react-hook-form.com/docs/useform/subscribe)APIで、再描画を起こさずにコールバックだけを呼びます。React Compilerを使っている場合、[watchの公式ドキュメント](https://react-hook-form.com/docs/useform/watch)は、`watch`の参照が変わらないためにコンパイラが結果をキャッシュし、古い値を表示しうると注意しています。コンパイラ適用下では`useWatch`を使うのが公式の案内です。

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

type Plan = { plan: "free" | "pro"; note: string };

function PlanNote({ control }: { control: Control<Plan> }) {
  const plan = useWatch({ control, name: "plan" }); // 再描画はこの子だけ
  return plan === "pro" ? <p>請求先の入力が必要です</p> : null;
}

export function PlanForm() {
  const { register, control, subscribe } = useForm<Plan>({
    defaultValues: { plan: "free", note: "" },
  });

  useEffect(() => {
    // 再描画せずに下書きを保存する
    return subscribe({
      name: "note",
      formState: { values: true },
      callback: ({ values }) => localStorage.setItem("draft-note", values.note),
    });
  }, [subscribe]);

  return (
    <form>
      <select {...register("plan")}>
        <option value="free">Free</option>
        <option value="pro">Pro</option>
      </select>
      <PlanNote control={control} />
      <textarea {...register("note")} />
    </form>
  );
}
```

## setValueによる値の更新とformStateによる状態管理

### setValueの再検証オプションと既定動作

住所の自動補完のように、プログラムから値を書き換えるのが`setValue`です。送信して「必須」エラーが出ている項目に`setValue`だけで値を入れても、エラー表示は消えませんでした。第3引数に`shouldValidate: true`を付けると、その場で再検証されて消えます。変更済みとして`isDirty`に反映させたいときは`shouldDirty: true`も付けます。

### isSubmittingによる送信ボタンの連打防止

`handleSubmit`に`async`関数を渡すと、Promiseが解決するまで`formState.isSubmitting`が`true`になります。実測でも、送信直後に`true`、完了後に`false`へ戻りました。ボタンの`disabled`に渡せば二重押しを防げます。

```
const {
  register,
  handleSubmit,
  setValue,
  formState: { isSubmitting, isDirty },
} = useForm<{ zip: string; address: string }>({
  defaultValues: { zip: "", address: "" },
});

const fillAddress = () =>
  setValue("address", "東京都千代田区", { shouldValidate: true, shouldDirty: true });

const onSubmit = async (data: { zip: string; address: string }) => {
  await fetch("/api/contact", { method: "POST", body: JSON.stringify(data) });
};

// JSX
<button type="submit" disabled={isSubmitting || !isDirty}>送信</button>
```

## ControllerによるMUIなどのUIライブラリとの連携

Material UIの`TextField`のように、値を`value`と`onChange`で受け取る制御コンポーネントには、`register`を展開する代わりに`Controller`を使います。`render`が受け取る`field`から`ref`だけを取り出して`inputRef`へ渡し、残りを展開します。エラーは`fieldState.error`で表示します。

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

export function MuiForm() {
  const { control, handleSubmit } = useForm({ defaultValues: { firstName: "" } });

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <Controller
        name="firstName"
        control={control}
        rules={{ required: "必須です" }}
        render={({ field: { ref, ...field }, fieldState }) => (
          <TextField
            {...field}
            inputRef={ref}
            label="名前"
            error={!!fieldState.error}
            helperText={fieldState.error?.message}
          />
        )}
      />
      <button type="submit">送信</button>
    </form>
  );
}
```

`ref`を分けずに`{...field}`をそのまま展開すると、`field.ref`が`TextField`の外側の要素に付き、`setFocus`や送信時の自動フォーカスでエラー項目の入力欄へ移れなくなります。`{...field}`に`inputRef={field.ref}`を足すだけでも、展開された`ref`が外側に付くため同じです。詳しい検証は[MUIとReact Hook Formの連携方法](/tech/details/4113/)にまとめています。

## useFieldArrayによる入力行の追加・削除

注文明細のように行を増減させるフォームには`useFieldArray`を使います。[公式リファレンス](https://react-hook-form.com/docs/usefieldarray)のとおり、各行の`key`には配列の添字ではなく`field.id`を渡します。公式は、添字をkeyにすると行の追加・削除で再描画が壊れるとしています。

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

type Order = { items: { product: string; qty: number }[] };

export function OrderForm() {
  const { register, control, handleSubmit } = useForm<Order>({
    defaultValues: { items: [{ product: "", qty: 1 }] },
  });
  const { fields, append, remove } = useFieldArray({ control, name: "items" });

  return (
    <form onSubmit={handleSubmit((data) => console.log(data.items))}>
      {fields.map((field, index) => (
        <div key={field.id}>
          <input {...register(`items.${index}.product`, { required: true })} />
          <input
            type="number"
            {...register(`items.${index}.qty`, { valueAsNumber: true, required: "数量は必須です", min: 1 })}
          />
          <button type="button" onClick={() => remove(index)}>削除</button>
        </div>
      ))}
      <button type="button" onClick={() => append({ product: "", qty: 1 })}>
        行を追加
      </button>
      <button type="submit">注文</button>
    </form>
  );
}
```

この`field.id`はv8で`field.key`に変わる箇所なので、移行時に最初に置き換えることになります。

## React Hook Formを使わないほうがよい場面

入れれば必ず得をするライブラリではありません。次の条件に当たるなら、まず使わない構成を検討してください。

- **入力欄が1〜2個で検証がほぼ無い**：検索ボックスやニュースレター登録はuseStateで十分です。依存を1つ増やす理由になりません。
- **React 19のform actionだけで送信が完結する**：サーバー側で検証してエラーを返すだけなら、`useActionState`とFormDataで足ります。書き方は[React 19のform actionの使い方](/tech/details/2851/)を参照してください。クライアント側の即時検証も欲しくなったら、[公式のAdvanced Usage](https://react-hook-form.com/advanced-usage)にある、`handleSubmit`で検証してから`startTransition`内でaction関数を呼ぶ併用が使えます。
- **型推論をフォーム全体で厳密に効かせたい**：v7の公開型には`any`が残ります。[TanStack Formとの違い](/tech/details/6350/)も比べてから選ぶと後悔が少なくなります。

検証ルールを複数の項目で管理する、入力→確認→送信の画面間で値を共有する、といった要件では、React Hook Formを採用する理由があります。確認画面を挟む実装は[Reactで確認画面付きフォームを作る方法](/tech/details/3793/)で解説しています。アンケートや申込フォームを業務システムとして作り込む場合は、弊社の[アンケート管理システム開発](/service/system/enquetesystem/)でも設計から対応しています。

## よくある質問

### React Hook Formの読み方は？

「リアクトフックフォーム」と読みます。npmのパッケージ名は`react-hook-form`で、略して「RHF」と書かれることもあります。

### React Hook Formを使うメリットは？

入力欄を非制御で扱い、値や状態の購読範囲を絞ることで、フォーム全体の再描画を抑えられることです。検証・エラー表示・送信中の状態を`useForm`1つにまとめられ、依存パッケージも0です。

### ref={register} と書くとエラーになるのはなぜ？

v6までの書き方だからです。7.91.0で`ref={register}`のまま入力して送信すると、`TypeError: input.split is not a function`で止まりました。v7では`{...register("name")}`とスプレッドします。

### errorsがundefinedになるのはなぜ？

v7では`errors`を`useForm()`の戻り値から直接は取れません。`const { errors } = useForm()`は`undefined`になり、`errors.name`で`TypeError`が出ます。`formState: { errors }`のように`formState`から取り出してください。

### React Hook Formのv8はもう使うべき？

2026年10月時点ではベータ（8.0.0-beta.5）なので、本番はv7.91.0のままで問題ありません。試す場合は`@beta`ではなく`@8.0.0-beta.5`と版を指定してください。

## 関連記事

- [ReactのZodフォームバリデーション実装｜zodResolverの導入とrefine・Zod 4の落とし穴](/tech/details/2371/)
- [MUIとReact Hook Formの連携方法｜インストールからControllerとsetFocusの落とし穴まで](/tech/details/4113/)
- [TanStack Formとは？使い方とReact Hook Formとの違い・zod/yup検証の落とし穴【v1.33】](/tech/details/6350/)
- [Reactで確認画面付きフォームを作る方法（入力→確認→送信とデータ保持）](/tech/details/3793/)
- [Reactフック（Hooks）とは？全18種の一覧とReact Compiler時代の使い分け【2026年8月版】](/tech/details/3671/)

---

出典: [React Hook Formとは？使い方とv7の書き方・検証タイミング【7.91対応】](<https://www.issoh.co.jp/tech/details/2898/>)（株式会社一創）
