「バリデーションってどうやって実装するの?」 「フォームの状態管理がごちゃごちゃしてきた…」
Reactでフォームを実装しようとすると、意外と悩むポイントが多いものです。入力値の管理、バリデーション、エラーメッセージの表示など、考慮すべき要素が次々と出てきますよね。
V0とCursorを活用することで、これらの実装を効率的に進めることができます。実際の開発の流れを見ていきましょう。
V0での開発スタート
まず、V0に以下のように依頼して基本的なログインフォームを生成しました:
ログインフォームを作成してください:
- メールアドレスとパスワードの入力欄
- バリデーション機能
- TailwindCSSでスタイリング
- エラー表示機能
V0は以下のようなコードを生成してくれました:
jsx
function LoginForm() {
const [formData, setFormData] = useState({
email: '',
password: ''
});
const [errors, setErrors] = useState({});
const handleSubmit = (e) => {
e.preventDefault();
// 基本的なバリデーション
const newErrors = {};
if (!formData.email) newErrors.email = '必須項目です';
if (!formData.password) newErrors.password = '必須項目です';
setErrors(newErrors);
};
// ... 残りの実装
}
Cursorでの改善
生成されたコードをベースに、Cursorを使って改善していきましょう。
jsx
import React, { useState, useEffect } from 'react';
const LoginForm = () => {
const [formData, setFormData] = useState({
email: '',
password: ''
});
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [touchedFields, setTouchedFields] = useState({
email: false,
password: false
});
// パスワードの強度チェック
const validatePassword = (password) => {
const errors = [];
if (password.length < 8) {
errors.push('8文字以上である必要があります');
}
if (!/[A-Z]/.test(password)) {
errors.push('大文字を含める必要があります');
}
if (!/[a-z]/.test(password)) {
errors.push('小文字を含める必要があります');
}
if (!/[0-9]/.test(password)) {
errors.push('数字を含める必要があります');
}
return errors;
};
// フォームの検証
const validateField = (name, value) => {
if (!value) {
return `${name === 'email' ? 'メールアドレス' : 'パスワード'}は必須です`;
}
if (name === 'email' && !/\S+@\S+\.\S+/.test(value)) {
return '有効なメールアドレスを入力してください';
}
if (name === 'password') {
const passwordErrors = validatePassword(value);
if (passwordErrors.length > 0) {
return passwordErrors.join(', ');
}
}
return '';
};
// 入力値の変更処理
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
// タッチ済みのフィールドのみバリデーション
if (touchedFields[name]) {
const error = validateField(name, value);
setErrors(prev => ({
...prev,
[name]: error
}));
}
};
// フィールドからフォーカスが外れた時の処理
const handleBlur = (e) => {
const { name } = e.target;
setTouchedFields(prev => ({
...prev,
[name]: true
}));
const error = validateField(name, formData[name]);
setErrors(prev => ({
...prev,
[name]: error
}));
};
// フォーム送信処理
const handleSubmit = async (e) => {
e.preventDefault();
setIsSubmitting(true);
// 全フィールドの検証
const newErrors = {};
Object.keys(formData).forEach(key => {
const error = validateField(key, formData[key]);
if (error) newErrors[key] = error;
});
if (Object.keys(newErrors).length === 0) {
try {
// ここで実際のログイン処理を実装
await new Promise(resolve => setTimeout(resolve, 1000));
alert('ログイン成功!');
// ログイン成功後の処理
} catch (error) {
setErrors({ submit: 'ログインに失敗しました。' });
}
} else {
setErrors(newErrors);
}
setIsSubmitting(false);
};
return (
<div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow-md">
<h2 className="text-2xl font-bold mb-6 text-gray-800">ログイン</h2>
<form onSubmit={handleSubmit} className="space-y-6" noValidate>
<div>
<label
htmlFor="email"
className="block text-sm font-medium text-gray-700 mb-1"
>
メールアドレス
</label>
<input
id="email"
type="email"
name="email"
value={formData.email}
onChange={handleChange}
onBlur={handleBlur}
className={`block w-full px-3 py-2 border rounded-md shadow-sm
focus:outline-none focus:ring-2
${errors.email
? 'border-red-300 focus:ring-red-500'
: 'border-gray-300 focus:ring-blue-500'}`}
aria-invalid={errors.email ? 'true' : 'false'}
aria-describedby={errors.email ? 'email-error' : undefined}
disabled={isSubmitting}
/>
{errors.email && (
<p
id="email-error"
className="mt-1 text-sm text-red-600"
role="alert"
>
{errors.email}
</p>
)}
</div>
<div>
<label
htmlFor="password"
className="block text-sm font-medium text-gray-700 mb-1"
>
パスワード
</label>
<input
id="password"
type="password"
name="password"
value={formData.password}
onChange={handleChange}
onBlur={handleBlur}
className={`block w-full px-3 py-2 border rounded-md shadow-sm
focus:outline-none focus:ring-2
${errors.password
? 'border-red-300 focus:ring-red-500'
: 'border-gray-300 focus:ring-blue-500'}`}
aria-invalid={errors.password ? 'true' : 'false'}
aria-describedby={errors.password ? 'password-error' : undefined}
disabled={isSubmitting}
/>
{errors.password && (
<p
id="password-error"
className="mt-1 text-sm text-red-600"
role="alert"
>
{errors.password}
</p>
)}
</div>
{errors.submit && (
<div
className="bg-red-50 border border-red-200 text-red-600 px-4 py-3 rounded"
role="alert"
>
{errors.submit}
</div>
)}
<button
type="submit"
disabled={isSubmitting}
className="w-full py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isSubmitting ? (
<span className="flex items-center justify-center">
<svg
className="animate-spin h-5 w-5 mr-2"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
ログイン中...
</span>
) : (
'ログイン'
)}
</button>
</form>
</div>
);
};
export default LoginForm;
1. バリデーションの強化
Cursorに以下のように質問します:
このログインフォームのバリデーションを改善したいです:
- メールアドレスの形式チェック
- パスワードの強度チェック(8文字以上、大文字小文字数字を含む)
- エラーメッセージの日本語化
実装方法を提案してください。
Cursorからの提案を取り入れて、以下のように改善しました:
2. ユーザー体験の改善
次に、Cursorに以下のように依頼して、UXを改善します:
フォームのUXを改善したいです:
- 送信中のローディング表示
- 入力中のリアルタイムバリデーション
- フォーカス時のスタイリング
まとめ
今回は、V0とCursorを活用したフォーム実装の基礎を学びました:
- V0でベースとなるコードを生成
- Cursorで機能の改善と最適化
- AIとの対話で段階的な機能追加
次回予告
次回は「シンプルなTodoアプリを作る」について学んでいきます。
フォームの知識を活かしながら、ToDoアプリの作成を通じてCRUD操作の基本を学んでいきましょう。