React Form Validation 実務パターン|実装コードとユースケース解説

React / Next.js

React Form Validation 実務パターン|実装コードとユースケース解説

Reactでアプリケーションを開発していると、フォーム検証は避けては通れない機能です。基本的なバリデーション処理から実務レベルの複雑な検証まで、実際に使えるパターンを詳しく解説します。

1. Reactフォーム検証の簡易的な解説

Reactでのフォーム検証とは、ユーザーが入力したデータが指定のルールを満たしているか確認し、不正なデータの送信を防ぐプロセスです。検証は大きく3つのタイミングで実施されます:

  • フィールド検証:入力中または入力後に各フィールドを個別に検証
  • フォーム検証:送信前にフォーム全体を検証
  • サーバー検証:バックエンド側での最終検証

Reactでは、状態管理とイベントハンドラーを組み合わせることで、ユーザーフレンドリーなフォーム検証を実装できます。

2. 業務でのユースケース

実務では、以下のようなユースケースがよく出現します:

ユースケース1:ECサイトの会員登録フォーム

メールアドレスの形式チェック、パスワード強度の検証、利用規約への同意確認など、複数の検証ルールが必要。リアルタイムフィードバックでユーザビリティを向上させる必要があります。

ユースケース2:管理画面の複雑なデータ入力フォーム

複数フィールドの相互参照検証(例:終了日は開始日より後である必要があるなど)、非同期検証(重複チェック)、条件付き検証が必要です。

ユースケース3:APIとの連携フォーム

送信前の検証だけでなく、バックエンド側のエラーレスポンスをフロント側で処理し、適切にユーザーにエラーメッセージを表示する必要があります。

3. 実装コード

3.1 基本的な実装(React Hooks + TypeScript)

まずは、useStateを使った基本的なフォーム検証の実装です:

import React, { useState, ChangeEvent, FormEvent } from 'react';

interface FormData {
  email: string;
  password: string;
  confirmPassword: string;
}

interface FormErrors {
  email?: string;
  password?: string;
  confirmPassword?: string;
}

const SimpleFormValidation: React.FC = () => {
  const [formData, setFormData] = useState({
    email: '',
    password: '',
    confirmPassword: '',
  });
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState>({});

  const validateField = (name: string, value: string): string | undefined => {
    switch (name) {
      case 'email':
        if (!value) return 'メールアドレスは必須です';
        if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value)) {
          return '有効なメールアドレスを入力してください';
        }
        return undefined;
      case 'password':
        if (!value) return 'パスワードは必須です';
        if (value.length < 8) return 'パスワードは8文字以上である必要があります';
        if (!/[A-Z]/.test(value)) return '大文字を含める必要があります';
        if (!/[0-9]/.test(value)) return '数字を含める必要があります';
        return undefined;
      case 'confirmPassword':
        if (!value) return 'パスワード確認は必須です';
        if (value !== formData.password) {
          return 'パスワードが一致しません';
        }
        return undefined;
      default:
        return undefined;
    }
  };

  const handleChange = (e: ChangeEvent) => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value,
    }));

    // 入力中に検証(touchedの場合のみ)
    if (touched[name]) {
      const error = validateField(name, value);
      setErrors(prev => ({
        ...prev,
        [name]: error,
      }));
    }
  };

  const handleBlur = (e: ChangeEvent) => {
    const { name, value } = e.target;
    setTouched(prev => ({
      ...prev,
      [name]: true,
    }));

    const error = validateField(name, value);
    setErrors(prev => ({
      ...prev,
      [name]: error,
    }));
  };

  const handleSubmit = (e: FormEvent) => {
    e.preventDefault();

    // 全フィールドを検証
    const newErrors: FormErrors = {};
    Object.keys(formData).forEach(key => {
      const error = validateField(key, formData[key as keyof FormData]);
      if (error) {
        newErrors[key as keyof FormErrors] = error;
      }
    });

    setErrors(newErrors);

    // エラーがなければ送信
    if (Object.keys(newErrors).length === 0) {
      console.log('フォーム送信:', formData);
      // ここでAPIにPOSTリクエストを送信
    }
  };

  return (
    
{errors.email && {errors.email}}
{errors.password && {errors.password}}
{errors.confirmPassword && ( {errors.confirmPassword} )}
); }; export default SimpleFormValidation;

3.2 react-hook-formを使った実装

実務では、react-hook-formという標準的なライブラリを使うことが多いです。複雑なフォーム検証を効率的に実装できます:

import React from 'react';
import { useForm, Controller, SubmitHandler } from 'react-hook-form';

interface IFormInput {
  email: string;
  password: string;
  confirmPassword: string;
  age: number;
  agreeToTerms: boolean;
}

const FormWithReactHookForm: React.FC = () => {
  const {
    register,
    handleSubmit,
    control,
    formState: { errors },
    watch,
  } = useForm({
    mode: 'onBlur',
    defaultValues: {
      email: '',
      password: '',
      confirmPassword: '',
      age: 0,
      agreeToTerms: false,
    },
  });

  const password = watch('password');

  const onSubmit: SubmitHandler = async (data) => {
    try {
      const response = await fetch('/api/register', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data),
      });
      if (!response.ok) throw new Error('登録に失敗しました');
      console.log('登録成功:', data);
    } catch (error) {
      console.error('エラー:', error);
    }
  };

  return (
    
{errors.email && {errors.email.message}}
/[A-Z]/.test(value) || '大文字を含める必要があります', hasNumber: (value) => /[0-9]/.test(value) || '数字を含める必要があります', }, })} /> {errors.password && {errors.password.message}}
value === password || 'パスワードが一致しません', })} /> {errors.confirmPassword && ( {errors.confirmPassword.message} )}
{errors.age && {errors.age.message}}
{errors.agreeToTerms && ( {errors.agreeToTerms.message} )}
); }; export default FormWithReactHookForm;

3.3 非同期検証とサーバーエラーハンドリング

実務では、メールアドレスの重複チェックなど、バックエンドとの通信を伴う検証が頻繁に発生します:

import React from 'react';
import { useForm, SubmitHandler } from 'react-hook-form';

interface IFormInput {
  email: string;
  username: string;
  password: string;
}

interface ServerError {
  field: string;
  message: string;
}

const AsyncValidationForm: React.FC = () => {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
    setError,
  } = useForm({
    mode: 'onBlur',
  });

  // メールアドレスの重複チェック(非同期検証)
  const checkEmailExists = async (email: string): Promise => {
    const response = await fetch('/api/check-email', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email }),
    });
    const data = await response.json();
    return data.exists;
  };

  // ユーザー名の重複チェック(非同期検証)
  const checkUsernameExists = async (username: string): Promise => {
    const response = await fetch('/api/check-username', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ username }),
    });
    const data = await response.json();
    return data.exists;
  };

  const onSubmit: SubmitHandler = async (data) => {
    try {
      const response = await fetch('/api/register', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data),
      });

      if (!response.ok) {
        const errorData = await response.json();
        // サーバーからのエラーをフォームにセット
        errorData.errors.forEach((error: ServerError) => {
          setError(error.field as keyof IFormInput, {
            message: error.message,
          });
        });
        return;
      }

      console.log('登録成功');
    } catch (error) {
      console.error('エラー:', error);
    }
  };

  return (
    
{ const exists = await checkEmailExists(value); return !exists || 'このメールアドレスは既に使用されています'; }, })} /> {errors.email && {errors.email.message}}
{ const exists = await checkUsernameExists(value); return !exists || 'このユーザー名は既に使用されています'; }, })} /> {errors.username && {errors.username.message}}
{errors.password && {errors.password.message}}
); }; export default AsyncValidationForm;

3.4 複雑な相互参照検証の実装

プロジェクト管理システムなど、複数フィールド間の関係性を検証する必要がある場合:

import React from 'react';
import { useForm, SubmitHandler } from 'react-hook-form';

interface IFormInput {
  projectName: string;
  startDate: string;
  endDate: string;
  budget: number;
  actualCost: number;
}

const ComplexValidationForm: React.FC = () => {
  const {
    register,
    handleSubmit,
    formState: { errors },
    watch,
  } = useForm();

  const startDate = watch('startDate');
  const endDate = watch('endDate');
  const budget = watch('budget');

  const onSubmit: SubmitHandler = (data) => {
    console.log('フォーム送信:', data);
  };

  return (
    
{errors.projectName && ( {errors.projectName.message} )}
{errors.startDate && {errors.startDate.message}}
{ if (!startDate) return true; return ( new Date(value) > new Date(startDate) || '終了日は開始日より後である必要があります' ); }, })} /> {errors.endDate && {errors.endDate.message}}
{errors.budget && {errors.budget.message}}
{ if (!budget) return true; return ( value <= budget || '実績費用は予算以下である必要があります' ); }, })} /> {errors.actualCost && {errors.actualCost.message}}
); }; export default ComplexValidationForm;

4. よくある応用パターン

4.1 動的フォーム(フィールドの動的追加・削除)

複数の住所入力や、複数の経歴情報など、ユーザーが入力項目を動的に追加できる場合があります:

import React from 'react';
import { useForm, useFieldArray, SubmitHandler } from 'react-hook-form';

interface IFormInput {
  name: string;
  addresses: Array<{
    street: string;
    city: string;
    zipCode: string;
  }>;
}

const DynamicFormExample: React.FC = () => {
  const { register, control, handleSubmit, formState: { errors } } = useForm({
    defaultValues: {
      name: '',
      addresses: [{ street: '', city: '', zipCode: '' }],
    },
  });

  const { fields, append, remove } = useFieldArray({
    control,
    name: 'addresses',
  });

  const onSubmit: SubmitHandler = (data) => {
    console.log('動的フォーム送信:', data);
  };

  return (
    
{errors.name && {errors.name.message}}
住所 {fields.map((field, index) => (
{fields.length > 1 && ( )}
))}
); }; export default DynamicFormExample;

4.2 条件付きフィールド検証

特定の条件下でのみ検証が必要なフィールドがあります:

import React from 'react';
import { useForm, SubmitHandler } from 'react-hook-form';

interface IFormInput {
  employmentType: 'employee' | 'contractor' | 'freelancer';
  companyName?: string;
  taxId?: string;
}

const ConditionalValidationForm: React.FC = () => {
  const { register, handleSubmit, formState: { errors }, watch } = useForm();
  const employmentType = watch('employmentType');

  const onSubmit: SubmitHandler = (data) => {
    console.log('条件付きフォーム送信:', data);
  };

  return (
    
{(employmentType === 'contractor' || employmentType === 'freelancer') && ( <>
{errors.companyName && ( {errors.companyName.message} )}
{errors.taxId && {errors.taxId.message}}
)}
); }; export default ConditionalValidationForm;

4.3 Zod/Yupによるスキーマベースの検証

複雑なバリデーションロジックを管理する場合、スキーマバリデーションライブラリの使用が効果的です:

import React from 'react';
import { useForm, SubmitHandler } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';

// Zodスキーマの定義
const formSchema = z.object({
email: z
.string()
.min(1, 'メールアドレスは必須です')
.email('有効なメールアドレスを入力してください'),
password: z
.string()
.min(8, 'パスワードは8文字以上である必要があります')
.regex(/[A-Z]/, '大文字を含める必要があります')
.regex(/[0-9]/, '数字を含める必要があります'),
confirmPassword: z.string(),
age: z
.number()
.min(18, '18歳以上である必要があります')
.max(120, '有効な年齢を入力してください'),
}).refine((data) => data.password === data.confirmPassword, {
message: 'パスワードが一致しません',
path: ['confirmPassword'],
});

type FormSchemaType = z.infer;

const ZodValidationForm: React.FC = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm({
resolver: zodResolver(formSchema),
mode: 'onBlur',
});

const onSubmit: SubmitHandler = (data) => {
console.log('Zod検証フォーム送信:', data);
};

return (



{errors.email && {errors.email.message}}


{errors.password && {errors.password.message}}


{errors.confirmPassword && (
{errors.confirmPassword.message}
)}


{errors.age && {errors.age.message}}

タイトルとURLをコピーしました