mcpField
F
form-validation-architect

form-validation-architect

github.com/curiositech開発257192 回インストール2 日前に更新
リポジトリ

form-validation-architect について

複雑な検証や操作性を扱うReact開発者向けのフォーム設計スキルです。react-hook-formとZodを中心に、型安全性、複数ステップのフロー、非同期検証、自動保存を扱います。

主な機能

  1. 01react-hook-formのフォームパターン
  2. 02Zodスキーマと型推論
  3. 03複数ステップのウィザード設計
  4. 04動的なフィールド配列の処理
  5. 05非同期検証と自動保存

活用シーン

  1. 01検証付きユーザー登録フォームの実装
  2. 02複数ステップの購入フローの構築
  3. 03項目を追加・削除できるフォームの作成
  4. 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.json
assets/form-templates/
LoginForm.tsx
MultiStepWizard.tsx
RegistrationForm.tsx
references/
accessibility.md
file-upload.md
zod-patterns.md
scripts/
generate_form.ts
validate_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 + ZodFormik + Yupネイティブ HTML5
パフォーマンス⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
型安全性⭐⭐⭐⭐⭐⭐⭐⭐❌
バンドルサイズ8KB30KB0KB
開発者体験⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
フィールド配列⭐⭐⭐⭐⭐⭐⭐⭐⭐❌
非同期バリデーション⭐⭐⭐⭐⭐⭐⭐⭐❌

参考資料

  • /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 スキーマの設計 | マルチステップウィザード | フィールド配列 | 自動保存 | 非同期バリデーション

コメント