React

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

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

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

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

まとめ: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を直接参照し、入力値はライブラリ内部でも保持します。入力値やエラーなどの購読範囲を絞れば再描画を抑えられますが、親の再描画に伴って子も再描画される場合があります。公式サイトは依存パッケージを持たない点も特徴に挙げています。

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移行ガイドにはnpm install react-hook-form@betaと書かれていますが、2026年10月27日時点でnpmのbetaタグは8.0.0-beta.0(2025年12月公開)を指したままで、最新のbeta.5は入りません。v8を試すなら[email protected]と版を指定してください。

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のリリースノートに並ぶ破壊的変更を確認して移るのが安全です。

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

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リファレンスでは、送信前の検証方法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で報告され、修正PR #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フォームバリデーション実装、Valibotなど他のスキーマも同じ形で受ける仕組みはStandard Schemaの解説で扱っています。

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で追加されたAPIで、再描画を起こさずにコールバックだけを呼びます。React Compilerを使っている場合、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の連携方法にまとめています。

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

注文明細のように行を増減させるフォームには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の使い方を参照してください。クライアント側の即時検証も欲しくなったら、公式のAdvanced Usageにある、handleSubmitで検証してからstartTransition内でaction関数を呼ぶ併用が使えます。
  • 型推論をフォーム全体で厳密に効かせたい:v7の公開型にはanyが残ります。TanStack Formとの違いも比べてから選ぶと後悔が少なくなります。

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

よくある質問

React Hook Formの読み方は?

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

React Hook Formを使うメリットは?

入力欄を非制御で扱い、値や状態の購読範囲を絞ることで、フォーム全体の再描画を抑えられることです。検証・エラー表示・送信中の状態をuseForm1つにまとめられ、依存パッケージも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と版を指定してください。

関連記事

お気に入りに入れた記事の一覧

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

この記事は以下の記事からリンクされています

ほか 8 件の記事からもリンクされています。

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.28 テックブログ リンツ不正アクセス:314万件の対象確認とパスワード強制リセット後の対処
  2. 2026.10.29 テックブログ ソニー生命の不正アクセス:38.5万件の対象確認と窓口、停止中の手続き
  3. 2026.10.27 テックブログ Brotli 脆弱性:CVE-2026-111497の影響確認とNode.js・Python・.NETの更新手順
  4. 2026.01.30 コラム 税務調査は個人と法人で何が違う?担当部署・件数・調べられる点と追徴税額を比較【令和6事務年度】
  5. 2026.09.26 コラム 価格転嫁とは?価格交渉促進月間の調査に回答義務はあるか・取適法の協議記録まで

RELATED POSTS 関連記事

目次