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 (
);
};
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 (
);
};
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 (
);
};
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 (
);
};
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 (
);
};
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 (
);
};
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 (

