React Hook FormのuseFormに入門してみる
まずuseFormは何か?
Reactでフォームを扱う時、「入力欄の状態管理」や「バリデーション」、「送信時の処理」などをどうするかが悩みどころです。その面倒な部分をまるっと助けてくれるのがreact-hook-formで、その中核にあるのがuseForm()です。
useFormを使うと、
- 入力値の状態管理(useState書かなくてOK)
- バリデーションチェック
- データ送信
- 初期値設定
などが一括で管理できます。
例で見るuseForm
import { useForm, SubmitHandler } from 'react-hook-form';
type FormValues = {
name: string;
};
const MyForm = () => {
const { register, handleSubmit } = useForm<FormValues>();
const onSubmit: SubmitHandler<FormValues> = (data) => {
console.log(data); // 例: { name: '入力された値' }
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('name')} />
<button type="submit">送信</button>
</form>
);
};
useForm<FormValues>()のようにジェネリクスで型を指定することで、フォームの値に型安全性を持たせることができます。registerに渡す名前もFormValuesのキーに制限されるため、タイポ防止にもなります。
registerとは何か?
質問:「registerとhandleSubmitってなに?」
register('name')は、React Hook Formに「このinputタグを名前付きで管理してね」と伝える命令です。これをやることで、値が自動的にstateに格納されるようになります。つまり入力値が自動で管理されます。
handleSubmitとは何か?
これはフォーム送信時に呼び出される関数で、内部でバリデーションをしてくれます。バリデーションを通った場合だけ、onSubmitが呼ばれる仕組みです。
なんでスプレッド構文...register("name")を使うの?
疑問:「なんで...register()って書くのか。registerだけでいいんじゃないのか?」
実はregister('name')はこんなオブジェクトを返しています。
{
onChange: ..., // 入力時の処理
onBlur: ...,
name: 'name',
ref: ...
}
これを<input>に直接渡すには、...(スプレッド構文)で展開してあげる必要があります。つまり「必要なpropsをまとめて突っ込むため」に使います。
スプレッド構文を使用することで下のような感じでpropsをまとめて渡せます。
<input
onChange={...}
onBlur={...}
name="name"
ref={...}
/>
バリデーションを追加する
registerの第2引数にバリデーションルールを渡せます。エラーメッセージはformState.errorsから取り出せます。
import { useForm, SubmitHandler } from 'react-hook-form';
type FormValues = {
name: string;
age: number;
};
const MyForm = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormValues>();
const onSubmit: SubmitHandler<FormValues> = (data) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register('name', {
required: '名前は必須です',
minLength: { value: 2, message: '2文字以上入力してください' },
})}
/>
{errors.name && <p>{errors.name.message}</p>}
<input
type="number"
{...register('age', {
required: '年齢は必須です',
min: { value: 0, message: '0以上で入力してください' },
})}
/>
{errors.age && <p>{errors.age.message}</p>}
<button type="submit">送信</button>
</form>
);
};
バリデーションが通らない限り
onSubmitは呼ばれないため、ハンドラ内で受け取るdataは「バリデーション済みの安全なデータ」と考えられます。
FormProviderとuseFormContextについて
FormProviderとuseFormContextは、フォームを複数のコンポーネントに分けて使いたいときに便利な仕組みです。
コード例
import { useForm, FormProvider, useFormContext, SubmitHandler } from 'react-hook-form';
type FormValues = {
email: string;
};
const InputField = () => {
const methods = useFormContext<FormValues>(); // FormProviderから受け取れる!
return <input {...methods.register('email')} />;
};
const MyForm = () => {
const methods = useForm<FormValues>();
const onSubmit: SubmitHandler<FormValues> = (data) => {
console.log(data);
};
return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
<InputField />
<button type="submit">送信</button>
</form>
</FormProvider>
);
};
そもそもmethodsって何か?
methodsっていうのは、useForm()から返される「フォーム管理用の関数や状態が詰まった道具箱」みたいなものです。
その中にはregister、handleSubmit、formState、setValue、getValuesなどが入っています。
子コンポーネントに渡す必要がない場合は、
const { register, handleSubmit } = useForm()のように分割代入で必要な関数だけ取り出すのが一般的です。FormProviderを使う時だけmethodsとしてまとめて受け取るイメージで使い分けるとスッキリします。
useFormContext()って何してる?いつ実行される?
useFormContext()は、FormProviderで共有されたmethodsを中から取り出して使うための関数です。これを子コンポーネントで呼び出すことで、propsでわざわざ渡さなくても、フォーム情報を扱えるようになります。
実行されるタイミングは、Reactの通常のレンダリングフローの中です。つまり、そのコンポーネントが描画されるときにuseFormContext()が呼ばれて、methodsが取り出されます。
useFormContext()とuseForm()の違いはなに?
-
useForm():新しいフォーム管理オブジェクト(methods)を生成する。主に親コンポーネントで使う -
useFormContext():すでにFormProviderによって共有されたmethodsを取得する。子コンポーネントで使う
<FormProvider {...methods}>って具体的に何してる?
ReactのContext APIを使って、親コンポーネントで作成したmethods(箱)をコンポーネントツリー全体に共有しています。それにより、子コンポーネントではuseFormContext()を使うだけで同じmethodsにアクセスできるようになります。
| 関数/コンポーネント | 役割 |
|---|---|
useForm() |
フォームの箱を作る |
FormProvider |
その箱を「メンバー全員に共有」 |
useFormContext() |
その箱を「メンバーが開けて使えるようにする鍵」 |
補足:UIライブラリと組み合わせる時はController
MUIやChakra UIなどのUIライブラリを使う場合、registerがうまく動かないケースがあります(ライブラリ側がrefを独自に管理していたりするため)。
その場合はControllerコンポーネントを使うのが定石です。
import { useForm, Controller } from 'react-hook-form';
import { TextField } from '@mui/material';
type FormValues = {
name: string;
};
const MyForm = () => {
const { control, handleSubmit } = useForm<FormValues>();
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<Controller
name="name"
control={control}
defaultValue=""
render={({ field }) => <TextField {...field} label="名前" />}
/>
<button type="submit">送信</button>
</form>
);
};
「
registerで動かないUIライブラリにはController」と覚えておくと迷いません。
次のステップ:Zodとの組み合わせ
ここまでのregister内バリデーションは便利ですが、実務ではzodと組み合わせるのがデファクトスタンダードになっています。
なぜZod?
- スキーマからTypeScriptの型を自動生成できる(型と検証ロジックの二重管理が不要)
- 複雑なバリデーション(条件付き、ネスト、配列など)が書きやすい
- バックエンドと共通のスキーマを使い回せる
コード例
npm install zod @hookform/resolvers
import { useForm, SubmitHandler } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
// Zodスキーマを定義
const schema = z.object({
name: z.string().min(2, '2文字以上入力してください'),
age: z.number().min(0, '0以上で入力してください'),
});
// スキーマから型を自動生成
type FormValues = z.infer<typeof schema>;
const MyForm = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
});
const onSubmit: SubmitHandler<FormValues> = (data) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('name')} />
{errors.name && <p>{errors.name.message}</p>}
<input type="number" {...register('age', { valueAsNumber: true })} />
{errors.age && <p>{errors.age.message}</p>}
<button type="submit">送信</button>
</form>
);
};
z.infer<typeof schema>によってスキーマから型が自動生成されるので、型とバリデーションが完全に同期するのが最大の強みです。
まとめ
React Hook Formは、Reactのフォーム管理を圧倒的に楽にしてくれるライブラリです。
useFormでフォームの状態管理を一括化registerでinputを登録、handleSubmitで送信処理FormProvider+useFormContextで子コンポーネントへ情報を共有- UIライブラリと組み合わせる時は
Controller - 実務ではZod + zodResolverで型安全なフォームを実現
useStateで頑張ってフォームを書いている人は、ぜひ試してみてください!