フォームバリデーション(Form Validation)ライブラリ比較

3つのアプローチでフォームバリデーションを実装し、エラー表示・送信制御・スキーマ定義の違いを比較

共通の実装仕様

各デモでは「ユーザー登録フォーム」を実装しています。全フィールドにバリデーションが設定されており、エラー時はフィールド直下にメッセージが表示されます。

フィールド種別バリデーション
名前テキスト必須、2文字以上
メールアドレスemail必須、メール形式
パスワード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
tsx
'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
tsx
'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
tsx
'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 のみ)
tsx
'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 + ZodRHF + YupFormik + Yupカスタム実装
型安全性◎ TypeScript完全対応◎ InferType△ 手動型定義△ 手動型定義
バンドルサイズ✅ 小さい✅ 小さい⚠️ 中程度✅ 最小
再レンダリング✅ 最小限✅ 最小限⚠️ 多め設計次第
学習コスト中(Zodの学習が必要)中(Yupの学習が必要)低(直感的なAPI)低(Reactのみ)
スキーマ再利用✅ API連携にも使える✅ 再利用可能✅ 再利用可能
コード量少ない少ない中程度多い

選択のポイント

RHF + Zod — TypeScriptプロジェクトのスタンダード。スキーマからAPIのバリデーションまで一元管理したい場合に最適。

RHF + Yup — Zodより歴史が長くエコシステムが成熟。既存プロジェクトでYupを使っていればこちら。

Formik + Yup — APIがシンプルで学習コストが低い。パフォーマンスより可読性を優先する場合に。

カスタム実装 — 依存関係を増やしたくない小規模フォームや、完全な制御が必要な場合に。