フォームバリデーション(Form Validation)ライブラリ比較
3つのアプローチでフォームバリデーションを実装し、エラー表示・送信制御・スキーマ定義の違いを比較
共通の実装仕様
各デモでは「ユーザー登録フォーム」を実装しています。全フィールドにバリデーションが設定されており、エラー時はフィールド直下にメッセージが表示されます。
| フィールド | 種別 | バリデーション |
|---|---|---|
| 名前 | テキスト | 必須、2文字以上 |
| メールアドレス | 必須、メール形式 | |
| パスワード | password | 必須、8文字以上 |
| パスワード確認 | password | 必須、パスワードと一致 |
1. React Hook Form + Zod
TypeScript完全対応 — Zodスキーマから型を自動生成し、APIバリデーションと共有可能
特徴
- •
useForm+zodResolverでスキーマベースのバリデーション - •
z.object()でフィールドの型・制約を一元定義 - •
formState.errorsで各フィールドのエラーに即アクセス - •
handleSubmitが自動でバリデーションを実行し、成功時のみコールバックを呼ぶ
インストール
npm install react-hook-form zod @hookform/resolvers'use client';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const schema = z.object({
name: z.string().trim().min(2, '2文字以上で入力してください'),
email: z.string().trim().email('正しいメールアドレスを入力してください'),
password: z.string().min(8, '8文字以上で入力してください'),
confirm: z.string(),
}).refine(data => data.password === data.confirm, {
message: 'パスワードが一致しません',
path: ['confirm'],
});
type FormData = z.infer<typeof schema>;
export function RHFZodDemo() {
const {
register,
handleSubmit,
reset,
formState: { errors, isSubmitting, isSubmitSuccessful },
} = useForm<FormData>({ resolver: zodResolver(schema) });
const onSubmit = async (_data: FormData) => {
await new Promise(resolve => setTimeout(resolve, 500));
reset();
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4" noValidate>
<div className="bg-blue-50 border border-blue-200 rounded-md px-4 py-2 text-sm text-blue-700">
🔒 このデモはブラウザ内で完結しています。入力した情報はサーバーへ送信・保存されません。
</div>
<div>
<label className="block text-sm font-medium mb-1">名前</label>
<input
{...register('name')}
autoComplete="name"
className="w-full border rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="山田 太郎"
/>
{errors.name && <p className="text-red-500 text-sm mt-1">{errors.name.message}</p>}
</div>
<div>
<label className="block text-sm font-medium mb-1">メールアドレス</label>
<input
{...register('email')}
type="email"
autoComplete="email"
className="w-full border rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="example@email.com"
/>
{errors.email && <p className="text-red-500 text-sm mt-1">{errors.email.message}</p>}
</div>
<div>
<label className="block text-sm font-medium mb-1">パスワード</label>
<input
{...register('password')}
type="password"
autoComplete="new-password"
className="w-full border rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="8文字以上"
/>
{errors.password && <p className="text-red-500 text-sm mt-1">{errors.password.message}</p>}
</div>
<div>
<label className="block text-sm font-medium mb-1">パスワード確認</label>
<input
{...register('confirm')}
type="password"
autoComplete="new-password"
className="w-full border rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="パスワードを再入力"
/>
{errors.confirm && <p className="text-red-500 text-sm mt-1">{errors.confirm.message}</p>}
</div>
{isSubmitSuccessful && (
<p className="text-green-600 font-medium">✅ 送信成功!</p>
)}
<button
type="submit"
disabled={isSubmitting}
className="w-full bg-blue-600 text-white rounded-md py-2 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-blue-700 transition-colors"
>
{isSubmitting ? '送信中...' : '送信'}
</button>
</form>
);
}🤖 AIプロンプトテンプレート
React + Tailwind CSSで、React Hook Form + Zodを使ったフォームバリデーションを実装してください。 - 使用ライブラリ: react-hook-form、zod、@hookform/resolvers/zod - z.object() でバリデーションスキーマを定義すること - zodResolver を useForm の resolver に渡すこと - z.infer<typeof schema> で型を自動生成すること - refine() でパスワード確認の一致チェックを実装すること - formState.errors から各フィールドのエラーメッセージを表示すること - handleSubmit のコールバックはバリデーション成功時のみ実行されることを活用すること
⚠️ このプロンプトはあくまでたたき台です。AIの回答はモデルやバージョン、会話の文脈によって毎回異なります。意図通りに動かない場合は、条件を追記・修正してお使いください。
2. React Hook Form + Yup
成熟したエコシステム — 歴史が長く、Formikとの相性も良いスキーマバリデーション
特徴
- •
useForm+yupResolverでスキーマベースのバリデーション - •
yup.object().shape()でフィールドの制約を定義 - •
.oneOf([yup.ref('password')])でパスワード確認の一致チェック - • Zodと同様の使用感だが、Yupはより歴史が長くFormikとの相性も良い
インストール
npm install react-hook-form yup @hookform/resolvers'use client';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
const schema = yup.object().shape({
name: yup.string().trim().min(2, '2文字以上で入力してください').required('必須です'),
email: yup.string().trim().email('正しいメールアドレスを入力してください').required('必須です'),
password: yup.string().min(8, '8文字以上で入力してください').required('必須です'),
confirm: yup.string()
.oneOf([yup.ref('password')], 'パスワードが一致しません')
.required('必須です'),
});
type FormData = yup.InferType<typeof schema>;
export function RHFYupDemo() {
const {
register,
handleSubmit,
reset,
formState: { errors, isSubmitting, isSubmitSuccessful },
} = useForm<FormData>({ resolver: yupResolver(schema) });
const onSubmit = async (_data: FormData) => {
await new Promise(resolve => setTimeout(resolve, 500));
reset();
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4" noValidate>
<div className="bg-blue-50 border border-blue-200 rounded-md px-4 py-2 text-sm text-blue-700">
🔒 このデモはブラウザ内で完結しています。入力した情報はサーバーへ送信・保存されません。
</div>
<div>
<label className="block text-sm font-medium mb-1">名前</label>
<input
{...register('name')}
autoComplete="name"
className="w-full border rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-purple-500"
placeholder="山田 太郎"
/>
{errors.name && <p className="text-red-500 text-sm mt-1">{errors.name.message}</p>}
</div>
<div>
<label className="block text-sm font-medium mb-1">メールアドレス</label>
<input
{...register('email')}
type="email"
autoComplete="email"
className="w-full border rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-purple-500"
placeholder="example@email.com"
/>
{errors.email && <p className="text-red-500 text-sm mt-1">{errors.email.message}</p>}
</div>
<div>
<label className="block text-sm font-medium mb-1">パスワード</label>
<input
{...register('password')}
type="password"
autoComplete="new-password"
className="w-full border rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-purple-500"
placeholder="8文字以上"
/>
{errors.password && <p className="text-red-500 text-sm mt-1">{errors.password.message}</p>}
</div>
<div>
<label className="block text-sm font-medium mb-1">パスワード確認</label>
<input
{...register('confirm')}
type="password"
autoComplete="new-password"
className="w-full border rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-purple-500"
placeholder="パスワードを再入力"
/>
{errors.confirm && <p className="text-red-500 text-sm mt-1">{errors.confirm.message}</p>}
</div>
{isSubmitSuccessful && (
<p className="text-green-600 font-medium">✅ 送信成功!</p>
)}
<button
type="submit"
disabled={isSubmitting}
className="w-full bg-purple-600 text-white rounded-md py-2 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-purple-700 transition-colors"
>
{isSubmitting ? '送信中...' : '送信'}
</button>
</form>
);
}🤖 AIプロンプトテンプレート
React + Tailwind CSSで、React Hook Form + Yupを使ったフォームバリデーションを実装してください。
- 使用ライブラリ: react-hook-form、yup、@hookform/resolvers/yup
- yup.object().shape() でバリデーションスキーマを定義すること
- yupResolver を useForm の resolver に渡すこと
- yup.InferType<typeof schema> で型を自動生成すること
- .oneOf([yup.ref('password')]) でパスワード確認の一致チェックを実装すること
- formState.errors から各フィールドのエラーメッセージを表示すること⚠️ このプロンプトはあくまでたたき台です。AIの回答はモデルやバージョン、会話の文脈によって毎回異なります。意図通りに動かない場合は、条件を追記・修正してお使いください。
3. Formik + Yup
シンプルなAPI — values / errors / handleChange を一括管理、学習コストが低い
特徴
- •
useFormikフックで values / errors / handleChange / handleSubmit を一括管理 - •
validationSchemaに Yup スキーマを渡すだけでバリデーションが動く - •
touchedを使ってフォーカスを外したフィールドのみエラーを表示 - • React Hook Formより再レンダリングが多いが、シンプルな構造で分かりやすい
インストール
npm install formik yup'use client';
import { useFormik } from 'formik';
import * as yup from 'yup';
const schema = yup.object().shape({
name: yup.string().trim().min(2, '2文字以上で入力してください').required('必須です'),
email: yup.string().trim().email('正しいメールアドレスを入力してください').required('必須です'),
password: yup.string().min(8, '8文字以上で入力してください').required('必須です'),
confirm: yup.string()
.oneOf([yup.ref('password')], 'パスワードが一致しません')
.required('必須です'),
});
export function FormikYupDemo() {
const formik = useFormik({
initialValues: { name: '', email: '', password: '', confirm: '' },
validationSchema: schema,
onSubmit: async (_values, { resetForm }) => {
await new Promise(resolve => setTimeout(resolve, 500));
resetForm();
},
});
return (
<form onSubmit={formik.handleSubmit} className="space-y-4" noValidate>
<div className="bg-blue-50 border border-blue-200 rounded-md px-4 py-2 text-sm text-blue-700">
🔒 このデモはブラウザ内で完結しています。入力した情報はサーバーへ送信・保存されません。
</div>
<div>
<label className="block text-sm font-medium mb-1">名前</label>
<input
name="name"
value={formik.values.name}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
autoComplete="name"
className="w-full border rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-orange-500"
placeholder="山田 太郎"
/>
{formik.touched.name && formik.errors.name && (
<p className="text-red-500 text-sm mt-1">{formik.errors.name}</p>
)}
</div>
<div>
<label className="block text-sm font-medium mb-1">メールアドレス</label>
<input
name="email"
type="email"
value={formik.values.email}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
autoComplete="email"
className="w-full border rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-orange-500"
placeholder="example@email.com"
/>
{formik.touched.email && formik.errors.email && (
<p className="text-red-500 text-sm mt-1">{formik.errors.email}</p>
)}
</div>
<div>
<label className="block text-sm font-medium mb-1">パスワード</label>
<input
name="password"
type="password"
value={formik.values.password}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
autoComplete="new-password"
className="w-full border rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-orange-500"
placeholder="8文字以上"
/>
{formik.touched.password && formik.errors.password && (
<p className="text-red-500 text-sm mt-1">{formik.errors.password}</p>
)}
</div>
<div>
<label className="block text-sm font-medium mb-1">パスワード確認</label>
<input
name="confirm"
type="password"
value={formik.values.confirm}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
autoComplete="new-password"
className="w-full border rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-orange-500"
placeholder="パスワードを再入力"
/>
{formik.touched.confirm && formik.errors.confirm && (
<p className="text-red-500 text-sm mt-1">{formik.errors.confirm}</p>
)}
</div>
{formik.submitCount > 0 && !formik.isSubmitting && !formik.dirty && (
<p className="text-green-600 font-medium">✅ 送信成功!</p>
)}
<button
type="submit"
disabled={formik.isSubmitting}
className="w-full bg-orange-500 text-white rounded-md py-2 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-orange-600 transition-colors"
>
{formik.isSubmitting ? '送信中...' : '送信'}
</button>
</form>
);
}🤖 AIプロンプトテンプレート
React + Tailwind CSSで、Formik + Yupを使ったフォームバリデーションを実装してください。
- 使用ライブラリ: formik、yup
- useFormik フックで initialValues・validationSchema・onSubmit を設定すること
- validationSchema に yup.object().shape() で定義したスキーマを渡すこと
- 各inputに name・value={formik.values.xxx}・onChange={formik.handleChange}・onBlur={formik.handleBlur} を設定すること
- formik.touched.xxx && formik.errors.xxx の条件でエラーを表示(onBlur後のみ表示)すること
- formik.handleSubmit を form の onSubmit に渡すこと⚠️ このプロンプトはあくまでたたき台です。AIの回答はモデルやバージョン、会話の文脈によって毎回異なります。意図通りに動かない場合は、条件を追記・修正してお使いください。
4. カスタム実装(useState のみ)
ライブラリ不要 — 依存関係を最小限に抑えた、Reactのみの実装
特徴
- • ライブラリ不要で依存関係が最小限
- •
useStateでフォームの値とエラーを管理 - • 送信時に自前のバリデーション関数を実行
- • 正規表現でメールアドレスの形式チェック
インストール
追加パッケージ不要(React + Tailwind CSS のみ)'use client';
import { useState } from 'react';
type FormValues = { name: string; email: string; password: string; confirm: string };
type FormErrors = Partial<FormValues>;
function validate(values: FormValues): FormErrors {
const errors: FormErrors = {};
const name = values.name.trim();
const email = values.email.trim();
if (!name || name.length < 2) errors.name = '2文字以上で入力してください';
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
errors.email = '正しいメールアドレスを入力してください';
}
if (!values.password || values.password.length < 8) errors.password = '8文字以上で入力してください';
if (values.confirm !== values.password) errors.confirm = 'パスワードが一致しません';
return errors;
}
export function CustomFormDemo() {
const [values, setValues] = useState<FormValues>({ name: '', email: '', password: '', confirm: '' });
const [errors, setErrors] = useState<FormErrors>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitted, setSubmitted] = useState(false);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValues(prev => ({ ...prev, [e.target.name]: e.target.value }));
setSubmitted(false);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const errs = validate(values);
setErrors(errs);
if (Object.keys(errs).length > 0) return;
setIsSubmitting(true);
await new Promise(resolve => setTimeout(resolve, 500));
setIsSubmitting(false);
setSubmitted(true);
setValues({ name: '', email: '', password: '', confirm: '' });
setErrors({});
};
return (
<form onSubmit={handleSubmit} className="space-y-4" noValidate>
<div className="bg-blue-50 border border-blue-200 rounded-md px-4 py-2 text-sm text-blue-700">
🔒 このデモはブラウザ内で完結しています。入力した情報はサーバーへ送信・保存されません。
</div>
<div>
<label className="block text-sm font-medium mb-1">名前</label>
<input
name="name"
value={values.name}
onChange={handleChange}
autoComplete="name"
className="w-full border rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-emerald-500"
placeholder="山田 太郎"
/>
{errors.name && <p className="text-red-500 text-sm mt-1">{errors.name}</p>}
</div>
<div>
<label className="block text-sm font-medium mb-1">メールアドレス</label>
<input
name="email"
type="email"
value={values.email}
onChange={handleChange}
autoComplete="email"
className="w-full border rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-emerald-500"
placeholder="example@email.com"
/>
{errors.email && <p className="text-red-500 text-sm mt-1">{errors.email}</p>}
</div>
<div>
<label className="block text-sm font-medium mb-1">パスワード</label>
<input
name="password"
type="password"
value={values.password}
onChange={handleChange}
autoComplete="new-password"
className="w-full border rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-emerald-500"
placeholder="8文字以上"
/>
{errors.password && <p className="text-red-500 text-sm mt-1">{errors.password}</p>}
</div>
<div>
<label className="block text-sm font-medium mb-1">パスワード確認</label>
<input
name="confirm"
type="password"
value={values.confirm}
onChange={handleChange}
autoComplete="new-password"
className="w-full border rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-emerald-500"
placeholder="パスワードを再入力"
/>
{errors.confirm && <p className="text-red-500 text-sm mt-1">{errors.confirm}</p>}
</div>
{submitted && <p className="text-green-600 font-medium">✅ 送信成功!</p>}
<button
type="submit"
disabled={isSubmitting}
className="w-full bg-emerald-600 text-white rounded-md py-2 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-emerald-700 transition-colors"
>
{isSubmitting ? '送信中...' : '送信'}
</button>
</form>
);
}🤖 AIプロンプトテンプレート
React + Tailwind CSSで、ライブラリ不要のカスタムフォームバリデーションを実装してください。 - 使用ライブラリ: React + Tailwind CSS のみ - useState でフォームの値(FormValues型)とエラー(FormErrors型)を別々に管理すること - validate() 関数でバリデーションロジックをまとめ、handleSubmit 時に実行すること - 正規表現でメールアドレスの形式チェックを実装すること - パスワード確認の一致チェックを実装すること - 送信成功時に「✅ 送信成功!」のメッセージを表示すること
⚠️ このプロンプトはあくまでたたき台です。AIの回答はモデルやバージョン、会話の文脈によって毎回異なります。意図通りに動かない場合は、条件を追記・修正してお使いください。
ライブラリ比較表
| 項目 | RHF + Zod | RHF + Yup | Formik + Yup | カスタム実装 |
|---|---|---|---|---|
| 型安全性 | ◎ TypeScript完全対応 | ◎ InferType | △ 手動型定義 | △ 手動型定義 |
| バンドルサイズ | ✅ 小さい | ✅ 小さい | ⚠️ 中程度 | ✅ 最小 |
| 再レンダリング | ✅ 最小限 | ✅ 最小限 | ⚠️ 多め | 設計次第 |
| 学習コスト | 中(Zodの学習が必要) | 中(Yupの学習が必要) | 低(直感的なAPI) | 低(Reactのみ) |
| スキーマ再利用 | ✅ API連携にも使える | ✅ 再利用可能 | ✅ 再利用可能 | ✗ |
| コード量 | 少ない | 少ない | 中程度 | 多い |
選択のポイント
RHF + Zod — TypeScriptプロジェクトのスタンダード。スキーマからAPIのバリデーションまで一元管理したい場合に最適。
RHF + Yup — Zodより歴史が長くエコシステムが成熟。既存プロジェクトでYupを使っていればこちら。
Formik + Yup — APIがシンプルで学習コストが低い。パフォーマンスより可読性を優先する場合に。
カスタム実装 — 依存関係を増やしたくない小規模フォームや、完全な制御が必要な場合に。