React Hook Formとは?使い方・バリデーション・v7の書き方を実例で解説
React Hook Form(リアクトフックフォーム)は、Reactでフォームの入力・検証・送信を、余分な再レンダリングを抑えながら書けるライブラリです。useStateで1項目ずつ状態を持つ方式と違い、入力欄を非制御(uncontrolled)で扱うため、フィールドが増えても再描画が広がりにくいのが特徴です。
2026年7月時点の最新は v7 系(7.81.0)で、v6 までの ref={register} という書き方は廃止され、{...register("name")} をスプレッドする形に変わっています。本記事は現行 v7 の書き方で、インストールから register・watch・setValue・formState・バリデーション・Zod連携・Controller までを実例で解説します。前提となる Hooks の基礎はReactフックの基本と導入もあわせて確認してください。
まとめ:React Hook Formの要点
- 何のライブラリか:Reactのフォーム状態管理・バリデーション用。入力欄を非制御で扱い、再レンダリングを最小化する。
- 導入:
npm install react-hook-form。React 16.8以上(Hooks対応版)で動作する。 - 基本形:
useForm()からregister・handleSubmit・formStateを取り出し、入力欄に{...register("フィールド名")}を展開する(v7の書き方)。 - 値の扱い:監視は
watch、プログラムからの更新はsetValue、エラーや送信中判定はformState.errors/formState.isSubmitting。 - 検証:
registerの第2引数(required・pattern等)か、Zod等のスキーマをresolverに渡す方法がある。 - 使い分け:入力欄が1〜2個の小さなフォームは
useStateで十分。項目数が多い・検証が複雑・再描画を抑えたい場面でReact Hook Formが効く。
React Hook Formの特徴と設計思想
読み方とライブラリの位置づけ
読み方は「リアクトフックフォーム」。React公式のフォーム機能ではなく、外部の軽量ライブラリで、依存パッケージを持たない点が採用されやすい理由の一つです。バンドルサイズが小さく、TypeScriptの型定義を標準で備えます。フォームの「入力値の保持・検証・送信」という共通処理を、Hooksの作法に沿って短いコードでまとめられます。
再レンダリングが少ない理由
React Hook Formは入力欄を非制御コンポーネントとして扱い、値の保持を ref 経由でDOMに委ねます。useState で1文字ごとに状態を更新する制御コンポーネント方式では、入力のたびにコンポーネントが再描画されますが、React Hook Formは購読(subscribe)した箇所だけを更新するため、フィールドが多いフォームほど描画コストの差が出ます。この ref の考え方はReactのuseRefの使い方で扱うDOM参照と同じ発想です。
インストールと基本的な使い方
npmでのインストール手順
パッケージマネージャでインストールします。動作にはReact 16.8以上(Hooks対応版)が必要です。
npm install react-hook-form
# yarn の場合
yarn add react-hook-form
useFormとregisterで作る最小フォーム
useForm() が返す register を入力欄に展開し、handleSubmit で送信を受け取ります。v6までの ref={register} ではなく、{...register("フィールド名")} とスプレッドするのがv7の書き方です。エラーは formState.errors から参照します。
import { useForm } from "react-hook-form";
export default function ContactForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm();
const onSubmit = (data) => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("name", { required: "名前は必須です" })} />
{errors.name && <span>{errors.name.message}</span>}
<input type="submit" />
</form>
);
}
handleSubmitで送信を処理する
handleSubmit(onSubmit) は、バリデーションを通過したときだけ第1引数の関数に入力値をまとめたオブジェクトを渡します。検証に失敗した場合は第2引数(省略可)にエラーが渡り、onSubmit は実行されません。非同期の送信処理をそのまま async 関数で書けます。
const onSubmit = async (data) => {
await fetch("/api/contact", {
method: "POST",
body: JSON.stringify(data),
});
};
入力値の取得・更新と状態管理
watchで値を監視する
watch は指定フィールド(または全体)の現在値を返し、値が変わると再描画をトリガーします。入力に応じて表示を切り替えるプレビューや条件付き表示に使います。監視対象が広いほど再描画は増えるため、必要なフィールドだけを指定するのが基本です。
const { register, watch } = useForm();
const name = watch("name"); // 特定フィールドを監視
const all = watch(); // フォーム全体を監視
setValueで値をプログラムから更新する
setValue は、他項目に連動した自動入力などで個別のフィールドを書き換えるときに使います。第3引数で shouldValidate(更新時に再検証)や shouldDirty(変更済みとして扱う)を指定できます。なお、フォーム全体の初期値は useForm({ defaultValues: {...} }) で与え、送信後に初期状態へ戻すときは reset() を使い分けます。
const { setValue } = useForm();
setValue("name", "山田太郎", {
shouldValidate: true,
shouldDirty: true,
});
formStateで送信状態やエラーを扱う
formState はフォームの状態をまとめたオブジェクトです。errors でバリデーションエラー、isSubmitting で送信中か、isDirty で初期値から変更されたかを判定できます。二重送信を防ぐには isSubmitting でボタンを無効化します。
const {
register,
handleSubmit,
formState: { errors, isSubmitting, isDirty },
} = useForm();
// JSX内:送信中はボタンを無効化して二重送信を防ぐ
<button type="submit" disabled={isSubmitting}>
送信
</button>
バリデーションの実装方法
registerのルールで検証する
手軽な検証は register の第2引数で指定します。required・minLength・maxLength・pattern などが使え、値をオブジェクトで渡すとエラーメッセージも同時に設定できます。メッセージは errors.フィールド名.message で取り出します。
<input
{...register("email", {
required: "メールアドレスは必須です",
pattern: {
value: /^[^@\s]+@[^@\s]+\.[^@\s]+$/,
message: "メール形式が正しくありません",
},
})}
/>
{errors.email && <span>{errors.email.message}</span>}
Zod(zodResolver)でスキーマ検証する
項目が増えて検証ルールが複雑になったら、スキーマライブラリと resolver を組み合わせます。Zodを使う場合は @hookform/resolvers と zod を追加し、zodResolver(schema) を useForm に渡します。検証ルールをスキーマ側に一元化でき、型もスキーマから導出できます。
npm install @hookform/resolvers zod
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.coerce.number().min(0, "0以上で入力してください"),
});
export default function Form() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm({ resolver: zodResolver(schema) });
const onSubmit = (data) => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("name")} />
{errors.name && <span>{errors.name.message}</span>}
<input type="number" {...register("age")} />
{errors.age && <span>{errors.age.message}</span>}
<button type="submit">送信</button>
</form>
);
}
ControllerでUIライブラリと連携する
Material UIのようにフォームの状態を内部で持つ制御コンポーネントは、{...register()} をそのまま展開できません。この場合は Controller でReact Hook Formの制御下に置きます。render が受け取る field をコンポーネントに渡し、fieldState.error でエラー表示を行います。Headless UIのような未装飾コンポーネントと組み合わせるときも同じ考え方です。
import { useForm, Controller } from "react-hook-form";
import { TextField } from "@mui/material";
export default function Form() {
const { control, handleSubmit } = useForm({
defaultValues: { firstName: "" },
});
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<Controller
name="firstName"
control={control}
rules={{ required: "必須です" }}
render={({ field, fieldState }) => (
<TextField
{...field}
label="名前"
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
<button type="submit">送信</button>
</form>
);
}
useStateやFormikと比べていつ使うべきか
React Hook Formは万能ではありません。入力欄が1〜2個で、値を即座に画面へ反映するだけの小さなフォームなら、useState で書いたほうが依存も増えず読みやすくなります。ライブラリを足す価値が出るのは、フィールド数が多い・バリデーションが複雑・再描画のコストを抑えたいという条件が重なる中〜大規模フォームです。
同種の代替として長く使われてきたFormik(最新は2.4.9)は今もメンテナンスされていますが、入力を制御コンポーネント前提で扱う設計のため、入力のたびに再描画が広がりやすい傾向があります。再描画コストや大規模フォームでの取り回しを重視するなら、非制御で動きTypeScript対応も手厚いReact Hook Formが有利です。逆に、すでにFormikで動いている制御フォーム中心のコードを、性能上の問題がないのに無理に移行する必要はありません。データ取得を伴う画面ではuseSWRなどのフックと役割を分けて組み合わせると見通しがよくなります。
よくある質問
React Hook Formの読み方は?
「リアクトフックフォーム」と読みます。パッケージ名は react-hook-form で、npmでインストールして使います。
ref={register} と書いてもエラーになるのはなぜ?
その書き方はv6までの旧APIです。v7以降は {...register("name")} をスプレッドする形に変わりました。古い記事のコードをそのまま貼ると動かないため、現行の書き方に置き換えてください。
errorsが取得できないときは?
v7では errors を useForm から直接は取り出せません。formState: { errors } のように formState 経由で分割代入します。
Material UIなどのコンポーネントで値が反映されない
UIライブラリの入力は制御コンポーネントなので、register の展開ではなく Controller で包み、field を渡してください。
送信ボタンの二重押しを防ぐには?
formState.isSubmitting を使い、送信中は disabled={isSubmitting} でボタンを無効化します。非同期の onSubmit が完了するまで自動でtrueになります。