form-validation-architect について
複雑な検証や操作性を扱うReact開発者向けのフォーム設計スキルです。react-hook-formとZodを中心に、型安全性、複数ステップのフロー、非同期検証、自動保存を扱います。
主な機能
- 01react-hook-formのフォームパターン
- 02Zodスキーマと型推論
- 03複数ステップのウィザード設計
- 04動的なフィールド配列の処理
- 05非同期検証と自動保存
活用シーン
- 01検証付きユーザー登録フォームの実装
- 02複数ステップの購入フローの構築
- 03項目を追加・削除できるフォームの作成
- 04長いフォームへの自動保存の追加
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ curiositech/ some_claude_skills --skill form-validation-architectインストール後、Claude Code で/form-validation-architectを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
.claude-plugin/plugin.jsonassets/form-templates/LoginForm.tsxMultiStepWizard.tsxRegistrationForm.tsxreferences/accessibility.mdfile-upload.mdzod-patterns.mdscripts/generate_form.tsvalidate_schemas.ts詳細
フォームバリデーションアーキテクト
クライアント側のバリデーション、型安全性、優れた UX を備えた、本番環境向けのフォームシステム構築に精通しています。
使用する場面
✅ 次の場合に使用します:
- 複数のフィールドとバリデーションルールを持つ複雑なフォーム
- 進捗状況を追跡できる複数ステップのウィザード
- 動的なフィールド配列(項目の追加・削除)
- セッションをまたいだフォーム状態の保持
- 非同期バリデーション(ユーザー名の使用可否確認、住所の検証)
- 依存するフィールド(A がチェックされたら B を有効にする)
- 進捗表示とバリデーションを備えたファイルアップロード
- 自動保存と楽観的更新
❌ 次の場合には使用しません:
- シンプルな問い合わせフォーム(HTML と基本的な JS で十分です)
- バックエンドのみのバリデーション(サーバー側で Joi、Yup を使用してください)
- React 以外のフレームワーク(Formik の代替を使用してください)
- 読み取り専用の表示(フォームは不要です)
簡易判断ツリー
Does your form:
├── Have >5 fields? → Use react-hook-form
├── Need type safety? → Add Zod schemas
├── Have dynamic fields? → Use field arrays
├── Span multiple steps? → Use wizard pattern
├── Need async validation? → Use resolver + async rules
└── Just email/message? → Use native HTML validation
技術の選定(2024年以降)
React Hook Form(推奨)
Formik より RHF を選ぶ理由:
- パフォーマンス:非制御入力 → 再レンダリングが少なくなります
- バンドルサイズ:8KB 対 30KB(Formik)
- 開発者体験:TypeScript のサポートが優れています
- 採用状況:スター数 40k 以上、2023年以降は業界標準です
変遷:
- 2015~2019年:Formik が主流でした
- 2019年:React Hook Form がリリースされました
- 2022年以降:RHF が標準になりました
- 2024年:Formik はメンテナンスモードです
スキーマバリデーションには Zod
Yup より Zod を選ぶ理由:
- TypeScript を優先:スキーマから型を推論できます
- 合成可能性:スキーマをより再利用しやすくなります
- エラーメッセージ:より柔軟にカスタマイズできます
- モダン:活発に開発され、最新機能が利用できます
変遷:
- 2017~2020年:Yup が標準でした
- 2020年:Zod がリリースされました
- 2023年以降:新規プロジェクトでは Zod が推奨されています
よくあるアンチパターン
アンチパターン 1:すべてを制御入力にする
初心者の考え方:「すべてのフォーム入力は useState で制御すべきです」
問題:キー入力のたびに再レンダリングが発生します
誤った方法:
// ❌ Re-renders entire component on every keystroke
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [name, setName] = useState('');
// ... 20 more useState calls
<input value={email} onChange={(e) => setEmail(e.target.value)} />
正しい方法:
// ✅ Uncontrolled with react-hook-form (minimal re-renders)
const { register, handleSubmit } = useForm();
<input {...register('email')} />
<input {...register('password')} />
<input {...register('name')} />
重要な理由:10 個以上のフィールドがあるフォームでは、制御入力によって動作が遅くなります。
アンチパターン 2:文字列ベースのバリデーション
問題:型安全性がなく、間違いが起きやすくなります
誤った方法:
// ❌ String validation, no types
const validate = (values) => {
if (!values.email.includes('@')) return 'Invalid email';
if (values.age < 18) return 'Must be 18+';
// Typo in field name? Runtime error!
};
正しい方法:
// ✅ Zod schema with type inference
const schema = z.object({
email: z.string().email('Invalid email'),
age: z.number().min(18, 'Must be 18+'),
username: z.string()
.min(3, 'Too short')
.regex(/^[a-z0-9_]+$/, 'Lowercase, numbers, underscores only')
});
type FormData = z.infer<typeof schema>; // Automatic TypeScript type!
変遷:
- 2020年以前:文字列ベースのバリデーションが一般的でした
- 2020年以降:スキーマ優先のバリデーションが標準になりました
- 2024年:スキーマからの型推論が期待されます
アンチパターン 3:エラー状態を管理しない
問題:ユーザーが操作する前にエラーが表示されます
誤った方法:
// ❌ Shows errors immediately on page load
{errors.email && <span>{errors.email}</span>}
正しい方法:
// ✅ Show errors only after field is touched
const { formState: { errors, touchedFields } } = useForm();
{touchedFields.email && errors.email && (
<span className="error">{errors.email.message}</span>
)}
// Or: Use mode="onBlur" to validate on blur
const form = useForm({
mode: 'onBlur' // Validate when user leaves field
});
重要な理由:UX が向上し、ユーザーは入力を始める前に叱られずに済みます。
アンチパターン 4:非同期バリデーションを行わない
問題:ユーザー名の使用可否や住所などを検証できません
正しい方法:
// ✅ Async validation with debounce
const schema = z.object({
username: z.string().refine(
async (username) => {
// Debounced API call
const available = await checkUsernameAvailability(username);
return available;
},
{ message: 'Username already taken' }
)
});
// Or: Custom async validation in RHF
register('username', {
validate: {
checkAvailable: async (value) => {
const response = await fetch(`/api/check-username?q=${value}`);
return response.ok || 'Username taken';
}
}
});
ベストプラクティス:API への過剰なリクエストを避けるため、非同期バリデーションにはデバウンスを適用します
アンチパターン 5:読み込み状態を表示しない
問題:ユーザーはバリデーションが実行中であることがわかりません
正しい方法:
// ✅ Show loading state during async validation
const { formState: { isValidating, isSubmitting } } = useForm();
<button disabled={isValidating || isSubmitting}>
{isSubmitting ? 'Submitting...' :
isValidating ? 'Checking...' :
'Submit'}
</button>
実装パターン
パターン 1:Zod を使った基本的なフォーム
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
// Define schema
const loginSchema = z.object({
email: z.string().email('Invalid email address'),
password: z.string().min(8, 'Password must be at least 8 characters'),
rememberMe: z.boolean().optional()
});
type LoginForm = z.infer<typeof loginSchema>;
function LoginForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting }
} = useForm<LoginForm>({
resolver: zodResolver(loginSchema),
defaultValues: {
rememberMe: false
}
});
const onSubmit = async (data: LoginForm) => {
await api.login(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input
{...register('email')}
type="email"
placeholder="Email"
/>
{errors.email && <span className="error">{errors.email.message}</span>}
</div>
<div>
<input
{...register('password')}
type="password"
placeholder="Password"
/>
{errors.password && <span className="error">{errors.password.message}</span>}
</div>
<div>
<label>
<input {...register('rememberMe')} type="checkbox" />
Remember me
</label>
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Logging in...' : 'Login'}
</button>
</form>
);
}
パターン 2:複数ステップのウィザード
const stepSchemas = [
// Step 1: Personal Info
z.object({
firstName: z.string().min(1, 'Required'),
lastName: z.string().min(1, 'Required'),
email: z.string().email()
}),
// Step 2: Address
z.object({
street: z.string().min(1, 'Required'),
city: z.string().min(1, 'Required'),
zipCode: z.string().regex(/^\d{5}$/, 'Invalid ZIP')
}),
// Step 3: Payment
z.object({
cardNumber: z.string().regex(/^\d{16}$/, 'Invalid card'),
expiry: z.string().regex(/^\d{2}\/\d{2}$/, 'MM/YY format'),
cvv: z.string().regex(/^\d{3}$/, '3 digits')
})
];
function MultiStepForm() {
const [step, setStep] = useState(0);
const [formData, setFormData] = useState({});
const form = useForm({
resolver: zodResolver(stepSchemas[step])
});
const nextStep = async () => {
const isValid = await form.trigger(); // Validate current step
if (isValid) {
setFormData({ ...formData, ...form.getValues() });
setStep(step + 1);
}
};
const prevStep = () => {
setFormData({ ...formData, ...form.getValues() });
setStep(step - 1);
};
const onSubmit = async (data) => {
const finalData = { ...formData, ...data };
await api.submitApplication(finalData);
};
return (
<div>
<progress value={step + 1} max={stepSchemas.length} />
<form onSubmit={form.handleSubmit(step === 2 ? onSubmit : nextStep)}>
{step === 0 && <PersonalInfoStep register={form.register} errors={form.formState.errors} />}
{step === 1 && <AddressStep register={form.register} errors={form.formState.errors} />}
{step === 2 && <PaymentStep register={form.register} errors={form.formState.errors} />}
<div>
{step > 0 && <button type="button" onClick={prevStep}>Back</button>}
<button type="submit">
{step === 2 ? 'Submit' : 'Next'}
</button>
</div>
</form>
</div>
);
}
パターン 3:動的なフィールド配列
const schema = z.object({
items: z.array(z.object({
name: z.string().min(1, 'Required'),
quantity: z.number().min(1, 'At least 1'),
price: z.number().min(0, 'Must be positive')
})).min(1, 'Add at least one item')
});
function OrderForm() {
const { register, control, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(schema),
defaultValues: {
items: [{ name: '', quantity: 1, price: 0 }]
}
});
const { fields, append, remove } = useFieldArray({
control,
name: 'items'
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
{fields.map((field, index) => (
<div key={field.id}>
<input
{...register(`items.${index}.name`)}
placeholder="Item name"
/>
<input
{...register(`items.${index}.quantity`, { valueAsNumber: true })}
type="number"
/>
<input
{...register(`items.${index}.price`, { valueAsNumber: true })}
type="number"
step="0.01"
/>
<button type="button" onClick={() => remove(index)}>
Remove
</button>
</div>
))}
<button type="button" onClick={() => append({ name: '', quantity: 1, price: 0 })}>
Add Item
</button>
<button type="submit">Submit Order</button>
</form>
);
}
パターン 4:自動保存(デバウンス)
import { useDebounce } from 'use-debounce';
import { useEffect } from 'react';
function AutosaveForm() {
const { watch, register } = useForm();
const formValues = watch(); // Watch all fields
// Debounce to avoid saving on every keystroke
const [debouncedValues] = useDebounce(formValues, 1000);
useEffect(() => {
// Save to localStorage or API
localStorage.setItem('draft', JSON.stringify(debouncedValues));
// Or: await api.saveDraft(debouncedValues);
}, [debouncedValues]);
return (
<form>
<input {...register('title')} placeholder="Title" />
<textarea {...register('content')} placeholder="Content" />
<small>Autosaved</small>
</form>
);
}
フォーム UX のベストプラクティス
1. 変更時ではなく、フォーカスが外れたときにバリデーションする
const form = useForm({
mode: 'onBlur' // Validate when user leaves field
// NOT 'onChange' - too aggressive
});
2. 無効な間は送信ボタンを無効にする
<button
type="submit"
disabled={!form.formState.isValid || form.formState.isSubmitting}
>
Submit
</button>
3. 送信時に最初のエラーへフォーカスする
const onSubmit = async (data) => {
try {
await api.submit(data);
} catch (error) {
// Focus first error field
const firstError = Object.keys(errors)[0];
form.setFocus(firstError);
}
};
4. 楽観的な UI 更新
const onSubmit = async (data) => {
// Optimistically update UI
setItems([...items, data]);
try {
await api.createItem(data);
} catch (error) {
// Rollback on error
setItems(items);
toast.error('Failed to save');
}
};
本番環境向けチェックリスト
□ Zod schemas for all forms
□ Type inference used (z.infer<typeof schema>)
□ Validation mode set appropriately (onBlur/onSubmit)
□ Error messages clear and actionable
□ Loading states for async operations
□ Focus management on errors
□ Autosave for long forms
□ Form state persisted (localStorage/session)
□ File upload progress indicators
□ Keyboard navigation tested
□ Accessibility (ARIA labels, error announcements)
□ Mobile-friendly (large touch targets)
使用する場面と避ける場面
| シナリオ | このスキルを使用しますか? |
|---|---|
| バリデーションを伴うユーザー登録 | ✅ はい |
| 複数ステップのチェックアウトフロー | ✅ はい |
| 動的なフォームビルダー | ✅ はい |
| シンプルなニュースレター登録 | ❌ いいえ - 標準の HTML を使用してください |
| バックエンドのみのバリデーション | ❌ いいえ - サーバー側で Joi/Yup を使用してください |
| React 以外のフレームワーク | ❌ いいえ - フレームワーク固有のソリューションを使用してください |
技術の比較
| 機能 | RHF + Zod | Formik + Yup | ネイティブ HTML5 |
|---|---|---|---|
| パフォーマンス | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 型安全性 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ❌ |
| バンドルサイズ | 8KB | 30KB | 0KB |
| 開発者体験 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ |
| フィールド配列 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ❌ |
| 非同期バリデーション | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ❌ |
参考資料
/references/zod-patterns.md- 高度な Zod スキーマパターン/references/accessibility.md- フォームのアクセシビリティガイドライン/references/file-upload.md- 進捗状況を追跡するファイルアップロード
スクリプト
scripts/generate_form.ts- Zod スキーマからフォームを生成scripts/validate_schemas.ts- よくある問題がないか Zod スキーマをリント
アセット
assets/form-templates/- すぐに使えるフォームコンポーネント
このスキルで扱う内容:フォームバリデーションのアーキテクチャ | react-hook-form のパターン | Zod スキーマの設計 | マルチステップウィザード | フィールド配列 | 自動保存 | 非同期バリデーション