React Hook Form 入門 — 仕組みを理解してから使う Part 1
読了時間の目安: 約10分 · React Hook Form v7 対応
「とりあえず動いた」ではなく、なぜ動くのかを理解しながら React Hook Form(以下 RHF)を使えるようになることを目標にします。この記事ではライブラリの導入理由・基本 API・よく誤解されるポイントを丁寧に解説します。
React でフォームを作るときの悩み
まず RHF を使う前に、素の React でフォームを管理するとどんな問題が起きるか整理しましょう。
| 問題 | 内容 |
|---|---|
| state が分散する | フィールドが増えるほど useState が増え、コンポーネントが肥大化する |
| 再レンダリングが多い | 1文字入力するたびに全フィールドが再レンダリングされる |
| バリデーションが複雑 | エラー管理・タイミング制御を自前で実装すると一貫性を保つのが難しい |
| 再利用しにくい | フォームのロジックが UI と密結合しており、別のフォームに流用できない |
これらは小さなフォームでは気にならないかもしれません。しかし項目が 10 個以上になったり、複雑なバリデーションが入ったりすると、一気に管理コストが跳ね上がります。
React Hook Form はどう解決するか
RHF の核心は uncontrolled components を優先する という設計思想です。
| controlled(通常の React) | uncontrolled(RHF のデフォルト) | |
|---|---|---|
| 値の管理 | React の state で管理 |
DOM 自身が値を持つ |
| 再レンダリング | 入力のたびに発生 | 入力中は発生しない |
| 値の取得 |
onChange で随時同期 |
送信時に ref 経由でまとめて取得 |
ポイント: RHF は「入力値を React state に同期しない」ことでパフォーマンスを担保しています。これが "高速" と言われる理由の本質です。
加えて、バリデーションのエラー管理・送信ハンドリング・デフォルト値の設定など、フォームに必要なロジックが useForm ひとつに集約されます。
インストールと最初の useForm
# npm
npm install react-hook-form
# yarn
yarn add react-hook-form
バージョン 7 以降、TypeScript のサポートが内蔵されているため別途 @types は不要です。
useForm が返す主なプロパティは次の 3 つです。
const {
register, // フィールドを RHF に登録する
handleSubmit, // 送信時のラッパー関数
formState, // errors・isSubmitting などの状態を持つオブジェクト
} = useForm();
基本的なフォームの実装例
名前・メール・パスワードの 3 フィールドを持つ登録フォームを作ってみます。
import { useForm } from "react-hook-form";
type FormValues = {
name: string;
email: string;
password: string;
};
export default function RegisterForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormValues>();
const onSubmit = (data: FormValues) => {
console.log(data); // バリデーション通過後のみ呼ばれる
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
{/* 名前 */}
<input
placeholder="名前"
{...register("name", {
required: "名前は必須です",
minLength: { value: 2, message: "2文字以上入力してください" },
})}
/>
{errors.name && <p>{errors.name.message}</p>}
{/* メール */}
<input
type="email"
placeholder="メールアドレス"
{...register("email", {
required: "メールアドレスは必須です",
pattern: {
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: "正しいメール形式で入力してください",
},
})}
/>
{errors.email && <p>{errors.email.message}</p>}
{/* パスワード */}
<input
type="password"
placeholder="パスワード (8文字以上)"
{...register("password", {
required: "パスワードは必須です",
minLength: { value: 8, message: "8文字以上入力してください" },
})}
/>
{errors.password && <p>{errors.password.message}</p>}
<button type="submit">登録</button>
</form>
);
}
よく誤解されるポイント 3 選
1. register は何をしているのか
{...register("name")} を spread すると、実際には次のプロパティが input に渡されます。
{
name: "name",
ref: refCallback, // DOM ノードを RHF の内部ストアに登録する
onChange: handler, // バリデーションのトリガーのみ(state 更新ではない)
onBlur: handler,
}
要点: ref によって DOM ノードへの参照を取得しているのが核心です。値は React state ではなく DOM の input.value に留まります。これが uncontrolled の実態です。
2. なぜ RHF は速いのか(もう少し具体的に)
通常の controlled form で 10 フィールドあると、次のような流れになります。
1文字入力 → setState → コンポーネント全体が再レンダリング → 10フィールド全部が再評価
RHF では入力中は React の state が変わらないため、そのコストがゼロです。エラー表示が必要になった瞬間だけ最小限の更新が走ります。フィールドが多いほど、この差は顕著に現れます。
3. formState はいつ再レンダリングを起こすか
formState は「購読した値が変わったときだけ」再レンダリングをトリガーします。
// errors だけ使う場合 → errors が変化したときだけ再レンダリング
const { formState: { errors } } = useForm();
// isSubmitting も使う場合 → errors または isSubmitting が変わると再レンダリング
const { formState: { errors, isSubmitting } } = useForm();
注意: formState を丸ごと変数に入れてしまうと、全プロパティを購読したとみなされます。必ず分割代入で必要なものだけ取り出しましょう。
// NG: 全プロパティを購読してしまう
const { formState } = useForm();
// OK: 必要なものだけ取り出す
const { formState: { errors, isSubmitting } } = useForm();
まとめ — どんなフォームに向いているか
RHF が特に力を発揮するケース:
- フィールド数が多い(5 個以上)登録・設定フォーム
- リアルタイムバリデーションが必要なフォーム
- Zod・Yup などのスキーマバリデーションと組み合わせたい場合
- フォームロジックをカスタムフックとして切り出して再利用したい場合
- パフォーマンスがシビアなモバイル向け UI
無理に使わなくてもよいケース:
- フィールドが 1〜2 個の単純な検索ボックスや問い合わせフォーム
- フォームの値を親コンポーネントとリアルタイムで同期しなければならない設計