フォーム
フロントエンド開発において、フォームとはエンドユーザーからの入力を受け取る代表的な手段である。名前やメールアドレス、検索キーワード、アンケートの回答など、ユーザーが操作するあらゆる入力データは、フォームを通してアプリケーションに伝えられる。
なぜ必要なのか
フォームは、ユーザーから情報を受け取り、アプリケーションがその情報をもとに処理を行うために必要不可欠である。ログインや検索、注文、問い合わせなど、ユーザーがサービスを操作するための入口となる。フォームがなければ、アプリは一方的な表示にとどまり、対話的な機能を持てなくなる。
制御コンポーネント
制御コンポーネントとは、フォームの入力値を State で管理するコンポーネントのこと。ユーザーが入力するたびに State が更新され、その State に基づいて表示がレンダリングされる。
特徴
- 入力値がリアルタイムに State と同期される
- 入力内容の検証や条件による表示制御が容易
- フォームの状態を一元管理できるため、複雑なフォームに向いている
コード例
「名前」と「年齢」を入力する制御コンポーネントのコード例を紹介する。
パターン①:useState()
を分けて管理する
特徴
- 各フィールドの状態が明確で、単純なフォームに向いている
- 状態が増えると
useState()
の行数が増えて冗長になりやすい
export const StateForm: React.FC = () => {
const [name, setName] = useState <string>('');
const [age, setAge] = useState <string>('');
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
alert(`名前: ${name}, 年齢: ${age}`);
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>名前:</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div>
<label>年齢:</label>
<input
type="number"
value={age}
onChange={(e) => setAge(e.target.value)}
/>
</div>
<button type="submit">送信</button>
</form>
);
};
パターン②:useState()
を1つにまとめて管理する
特徴
- 状態が1つにまとまっていて、フォームの項目が多い場合に便利
-
onChange
を 汎用化できる ので、同じロジックで複数フィールドに対応しやすい
type FormData = {
name: string;
age: string;
};
export const StateForm: React.FC = () => {
const [form, setForm] = useState <FormData> ({
name: '',
age: '',
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setForm((prev) => ({ ...prev, [name]: value }));
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
alert(`名前: ${form.name}, 年齢: ${form.age}`);
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>名前:</label>
<input
type="text"
name="name"
value={form.name}
onChange={handleChange}
/>
</div>
<div>
<label>年齢:</label>
<input
type="number"
name="age"
value={form.age}
onChange={handleChange}
/>
</div>
<button type="submit">送信</button>
</form>
);
};
useState()
vs useImmer()
制御コンポーネントでは、フォームの状態を useState()
で管理するのが基本だが、オブジェクトが入れ子構造になると記述が煩雑になりやすい。その場合、useImmer()
を使うとよりシンプルに状態更新ができる。
useState()
の場合(ネストあり)
特徴
- スプレッド構文が多くなり、ネストが深くなるほど書きづらくなる
- 更新ミスが起きやすく、読みづらい
type FormData = {
user: {
name: string;
age: string;
};
};
const [form, setForm] = useState<FormData>({
user: {
name: '',
age: '',
},
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setForm((prev) => ({
...prev,
user: {
...prev.user,
[name]: value,
},
}));
};
スプレッド構文によるオブジェクトのコピーは浅いコピーのため、ネストに弱い。
useImmer()
の場合
特徴
- スプレッド構文不要で、直接オブジェクトを更新するように書ける
- ネストが深くても直感的に扱える
-
useState()
よりも複雑なフォーム構造に向いている
import { useImmer } from 'use-immer';
type FormData = {
user: {
name: string;
age: string;
};
};
const [form, setForm] = useImmer<FormData>({
user: {
name: '',
age: '',
},
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setForm((form) => {
form.user[name as 'name' | 'age'] = value;
});
};
setForm((form) => { ... })
の form
は、Immer による「編集用のコピー」。見た目は普通のオブジェクトだが、直接書き換えてOK。
非制御コンポーネント
非制御コンポーネントとは、フォームの入力値を State で保持しないコンポーネントのことを。State を用いないため、入力値が欲しい場合は、DOM の参照を通じて直接値を取得する。
特徴
- 入力値の変更時に State 更新が発生しないため、再レンダリングが減りパフォーマンスに有利
- 実装がシンプルで、簡単なフォームに適している
- 入力値を取得するタイミングが限られ、リアルタイム検証には向かない
- State と同期しないため、UI の一元管理が難しい場合がある
コード例
特徴
-
useRef()
を使って DOM に直接アクセスし、値を取得する - 初期値の設定に必ず defaultValue を使う必要がある
export const UncontrolledForm: React.FC = () => {
const nameRef = useRef<HTMLInputElement>(null);
const ageRef = useRef<HTMLInputElement>(null);
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const name = nameRef.current?.value || '';
const age = ageRef.current?.value || '';
alert(`名前: ${name}, 年齢: ${age}`);
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>名前:</label>
<input
type="text"
defaultValue="太郎"
ref={nameRef}
/>
</div>
<div>
<label>年齢:</label>
<input
type="number"
defaultValue="20"
ref={ageRef}
/>
</div>
<button type="submit">送信</button>
</form>
);
};
React Hook Form
React Hook Form は、React アプリケーションにおけるフォームの実装と管理を効率化するためのライブラリ。React の フックベースの設計を活かして、フォームの入力値、バリデーション、エラーメッセージ、送信処理などを 最小限のコードでシンプルに扱えるのが特徴。
主な特徴
- 再レンダリングを最小限に抑えることでパフォーマンスが高い
- バリデーション機能が豊富(独自ルール+外部ライブラリとの統合も可)
- 型安全な開発が可能(TypeScript との親和性が高い)
なぜ必要なのか
React でフォームを実装する際、useState
を使って入力値を一つひとつ管理すると、コードが煩雑になりやすく、パフォーマンスの低下や状態管理のミスにつながることがある。React Hook Form は、少ない記述で効率よくフォームを構築・管理できるライブラリであり、再レンダリングの最小化やバリデーションの簡素化を通じて、保守性・パフォーマンス・ユーザー体験の向上を実現する。
基本的な使い方
import { useForm } from 'react-hook-form';
type FormData = {
name: string;
age: number;
};
export const BasicForm = () => {
const {
register, // フィールド登録
handleSubmit, // 送信処理
formState: { errors }, // バリデーションエラー
} = useForm<FormData>();
const onSubmit = (data: FormData) => {
alert(`名前: ${data.name}, 年齢: ${data.age}`);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label>名前:</label>
<input
{...register('name', { required: '名前は必須です' })}
/>
<p>{errors.name?.message}</p>
</div>
<div>
<label>年齢:</label>
<input
type="number"
{...register('age', {
required: '年齢は必須です',
min: { value: 0, message: '0歳以上を入力してください' },
})}
/>
<p>{errors.age?.message}</p>
</div>
<button type="submit">送信</button>
</form>
);
};
useForm()
React Hook Form の中心となるフック。フォームの状態管理、バリデーション、送信処理などを一括で扱えるようになる。
主な戻り値
-
register
: 入力項目を登録する関数 -
handleSubmit
: バリデーション後に onSubmit を実行するラッパー関数 -
formState.errors
: 各フィールドのバリデーションエラー
const {
register, // フィールド登録
handleSubmit, // 送信処理
formState: { errors }, // バリデーションエラー
} = useForm<FormData>();
register()
指定されたフィールドに対応するイベントハンドラー、参照などを登録する関数。検証ルールもここで同時に指定できる。
<input
{...register('name', { required: '名前は必須です' })}
/>
register()
の戻り値は onChange、onBlur、ref、name からなるオブジェクトである。スプレッド構文を使うことで以下と等価のコードになる。
<input
onChange={onChange}
onBlur={onBlur}
ref={ref}
name={name}
/>
handleSubmit()
フォームの送信イベントを処理するためのラッパー関数。バリデーションを実行し、成功した場合にのみコールバック(onSubmit)を実行する。
<form onSubmit={handleSubmit(onSubmit)}>
errors
各フィールドのバリデーションエラーを保持するオブジェクト。errors.<フィールド名>
にアクセスすることで、エラーメッセージなどを取得できる。
<p>{errors.name?.message}</p>
独自検証ルール
register
では、組み込み以外に独自の検証関数も渡せる。validate
に関数を渡し、条件に合わなければエラーメッセージを返すことで自由なバリデーションが可能。
<input
{...register('age', {
validate: value =>
value >= 18 || '18歳以上である必要があります'
})}
/>
フォームの状態管理
フォームの状態を formState
オブジェクトで管理している。主に以下のような状態を扱う。
プロパティ名 | 内容 |
---|---|
isDirty |
入力値が初期値から変更されたかどうか |
isValid |
全フィールドのバリデーションが成功しているかどうか |
isSubmitting |
フォーム送信中かどうか |
isSubmitted |
送信処理が完了したかどうか |
これを利用して、送信ボタンの活性/非活性制御や、送信中のローディング表示などができる。
const {
register,
handleSubmit,
formState: { errors, isDirty, isValid, isSubmitting },
} = useForm<FormData>({
mode: 'onChange', // 入力値変更時にバリデーションを実行し、isValidを更新
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
<button type="submit" disabled={!isDirty || !isValid || isSubmitting}>
{isSubmitting ? '送信中...' : '送信'}
</button>
</form>
);
検証ライブラリとの連携
React Hook Form では簡単なバリデーションルールを記述できるが、複雑なルールやネストされた構造の検証には限界がある。そのため、外部のスキーマベースの検証ライブラリと組み合わせるのが一般的。
ライブラリ | 特徴 |
---|---|
Yup | 最も一般的。直感的で柔軟なスキーマ記述が可能 |
Zod | TypeScript に強く、スキーマと型の一貫性が高い |
Joi | Node.js 向けに開発されたが、React でも利用可 |
Valibot | 軽量・高速で Zod 代替に注目されつつある |
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as Yup from 'yup';
type FormData = {
name: string;
age: number;
};
// Yup スキーマ定義
const schema = Yup.object().shape({
name: Yup.string()
.required('名前は必須です')
.min(2, '名前は2文字以上で入力してください'),
age: Yup.number()
.required('年齢は必須です')
.min(18, '18歳以上である必要があります')
.max(120, '120歳以下で入力してください'),
});
export const YupForm = () => {
const {
register,
handleSubmit,
formState: { errors, isDirty, isValid, isSubmitting },
} = useForm<FormData>({
resolver: yupResolver(schema), // Yup スキーマを適用
mode: 'onChange', // 入力変更時にバリデーション実行
});
const onSubmit = (data: FormData) => {
alert(`名前: ${data.name}, 年齢: ${data.age}`);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label>名前:</label>
<input {...register('name')} />
<p>{errors.name?.message}</p>
</div>
<div>
<label>年齢:</label>
<input type="number" {...register('age')} />
<p>{errors.age?.message}</p>
</div>
<button type="submit" disabled={!isDirty || !isValid || isSubmitting}>
{isSubmitting ? '送信中...' : '送信'}
</button>
</form>
);
};