mcpField
R
react-forms

react-forms

github.com/claude-dev-suite開発4642 回インストール4 日前に更新
リポジトリ

react-forms について

React 開発者向けのフォームパターンをまとめたスキルです。制御・非制御入力、React Hook Form、検証の実践例とトラブルシューティングを扱います。

主な機能

  1. 01制御・非制御フォーム
  2. 02React Hook Form のパターン
  3. 03フィールド配列と制御コンポーネント
  4. 04検証とアクセシビリティの実践
  5. 05送信時の問題の調査

活用シーン

  1. 01入力状態の管理方法の選択
  2. 02スキーマ検証の設定
  3. 03動的なフォーム項目の処理
  4. 04送信や検証の問題の調査

MCP 対応

React フォームのドキュメント取得先として mcp__documentation__fetch_docs を案内しています。

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/claude-dev-suite/claude-dev-suite --skill react-forms

インストール後、Claude Code で/react-formsを入力して読み込まれたか確認します。

同梱ファイルadvanced.md

詳細

React フォーム

詳細な知識: React のフォームパターンとバリデーション戦略に関する包括的なドキュメントは、technology: react、topic: forms を指定して mcp__documentation__fetch_docs を使用してください。

完全なリファレンス: Server Actions、マルチステップフォーム、依存フィールド、自動保存、バリデーションパターン、アクセシビリティについては、advanced.md を参照してください。

制御コンポーネントと非制御コンポーネント

制御フォーム

React の状態で入力を制御します。

function ControlledForm() {
  const [formData, setFormData] = useState({
    email: '',
    password: '',
  });
  const [errors, setErrors] = useState<Record<string, string>>({});

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

    if (errors[name]) {
      setErrors(prev => ({ ...prev, [name]: '' }));
    }
  };

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

    const newErrors: Record<string, string> = {};
    if (!formData.email) newErrors.email = 'Email required';
    if (!formData.password) newErrors.password = 'Password required';

    if (Object.keys(newErrors).length > 0) {
      setErrors(newErrors);
      return;
    }

    console.log('Submitting:', formData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          name="email"
          type="email"
          value={formData.email}
          onChange={handleChange}
        />
        {errors.email && <span className="error">{errors.email}</span>}
      </div>

      <div>
        <input
          name="password"
          type="password"
          value={formData.password}
          onChange={handleChange}
        />
        {errors.password && <span className="error">{errors.password}</span>}
      </div>

      <button type="submit">Submit</button>
    </form>
  );
}
非制御フォーム(useRef)

DOM で入力を制御します。

function UncontrolledForm() {
  const emailRef = useRef<HTMLInputElement>(null);
  const passwordRef = useRef<HTMLInputElement>(null);

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

    const data = {
      email: emailRef.current?.value,
      password: passwordRef.current?.value,
    };

    console.log('Submitting:', data);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input ref={emailRef} name="email" type="email" />
      <input ref={passwordRef} name="password" type="password" />
      <button type="submit">Submit</button>
    </form>
  );
}
FormData API(非制御)
function FormDataForm() {
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);

    const data = {
      email: formData.get('email') as string,
      password: formData.get('password') as string,
    };

    console.log('Submitting:', data);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="email" type="email" required />
      <input name="password" type="password" required />
      <button type="submit">Submit</button>
    </form>
  );
}

React Hook Form

最も人気のあるフォームライブラリです。優れた開発者体験を備えた非制御方式です。

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const schema = z.object({
  email: z.string().email('Invalid email'),
  password: z.string().min(8, 'Password must be at least 8 characters'),
  confirmPassword: z.string(),
}).refine(data => data.password === data.confirmPassword, {
  message: 'Passwords must match',
  path: ['confirmPassword'],
});

type FormData = z.infer<typeof schema>;

function SignUpForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
    reset,
  } = useForm<FormData>({
    resolver: zodResolver(schema),
    defaultValues: { email: '', password: '', confirmPassword: '' },
  });

  const onSubmit = async (data: FormData) => {
    try {
      await signUp(data);
      reset();
    } catch (error) {
      console.error('Sign up failed:', error);
    }
  };

  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>
        <input {...register('confirmPassword')} type="password" placeholder="Confirm Password" />
        {errors.confirmPassword && <span className="error">{errors.confirmPassword.message}</span>}
      </div>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Signing up...' : 'Sign Up'}
      </button>
    </form>
  );
}
フォーム配列
import { useFieldArray, useForm } from 'react-hook-form';

interface OrderForm {
  items: { productId: string; quantity: number }[];
  notes: string;
}

function OrderForm() {
  const { register, control, handleSubmit } = useForm<OrderForm>({
    defaultValues: {
      items: [{ productId: '', quantity: 1 }],
      notes: '',
    },
  });

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

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {fields.map((field, index) => (
        <div key={field.id} className="flex gap-2">
          <select {...register(`items.${index}.productId`)}>
            <option value="">Select product</option>
            {products.map(p => (
              <option key={p.id} value={p.id}>{p.name}</option>
            ))}
          </select>

          <input
            {...register(`items.${index}.quantity`, { valueAsNumber: true })}
            type="number"
            min="1"
          />

          <button type="button" onClick={() => remove(index)}>
            Remove
          </button>
        </div>
      ))}

      <button type="button" onClick={() => append({ productId: '', quantity: 1 })}>
        Add Item
      </button>

      <textarea {...register('notes')} placeholder="Notes" />
      <button type="submit">Submit Order</button>
    </form>
  );
}
RHF での制御コンポーネント
import { Controller, useForm } from 'react-hook-form';
import { DatePicker } from '@/components/date-picker';

function EventForm() {
  const { control, handleSubmit } = useForm();

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Controller
        name="date"
        control={control}
        rules={{ required: 'Date is required' }}
        render={({ field, fieldState }) => (
          <div>
            <DatePicker
              value={field.value}
              onChange={field.onChange}
            />
            {fieldState.error && (
              <span className="error">{fieldState.error.message}</span>
            )}
          </div>
        )}
      />

      <button type="submit">Create Event</button>
    </form>
  );
}

ベストプラクティス

することしないこと
複雑なフォームには React Hook Form を使用するフォームの状態をすべて手動で管理する
バリデーションには Zod/Yup を使用するバリデーションを一から実装する
インラインでエラーを表示するすべてのエラーを一度に表示する
aria-* 属性を使用するアクセシビリティを忘れる
負荷の高いバリデーションはデバウンスするキー入力のたびに非同期でバリデーションする
  • ✅ リアルタイムのバリデーションが必要な場合は、制御入力を使用します
  • ✅ パフォーマンスを高めるには、非制御入力(RHF)を使用します
  • ✅ UX を高めるには、フォーカスが外れたときにバリデーションします
  • ✅ 送信中は読み込み状態を表示します
  • ❌ フォームが空だからといって送信ボタンを無効にしないでください
  • ❌ エラー時にフォームをクリアしないでください
  • ❌ ユーザーが操作する前にエラーを表示しないでください

このスキルを使用しないケース

  • Zod スキーマを使った React Hook Form - 詳細なパターンについては react-hook-form スキルを使用してください
  • React 19 の Server Actions フォーム - useActionState と useFormStatus については react-19 スキルを使用してください
  • シンプルな入力処理 - 基本的な制御入力/非制御入力については react スキルを使用してください
  • RHF 以外のフォームライブラリ - ライブラリ固有のドキュメントに従ってください

アンチパターン

アンチパターン問題解決策
フォームの状態をすべて手動で管理するボイラープレート、バグReact Hook Form を使用する
ユーザー入力をバリデーションしないセキュリティ/UX 上の問題Zod のバリデーションスキーマを使用する
マウント時にすべてのエラーを表示するUX が悪いフォーカスが外れたとき、または送信時にバリデーションする
フォームが空のときに送信を無効にするバリデーションメッセージを表示できない送信を許可してエラーを表示する
バリデーションエラー時にフォームをクリアするユーザーの入力データが失われるフォームデータを保持してエラーを表示する
読み込み状態を表示しないUX が分かりにくい送信中は読み込み状態を表示する
アクセシビリティ属性が不足しているアクセシブルでないaria ラベルとロールを追加する
キー入力のたびに非同期でバリデーションするパフォーマンスの問題非同期バリデーションをデバウンスする

すばやいトラブルシューティング

問題考えられる原因修正方法
フォームが送信されないonSubmit または action がないフォームに onSubmit ハンドラーを追加する
バリデーションが機能しないスキーマが適用されていないuseForm の resolver を確認する
入力が更新されない正しく制御されていないvalue と onChange が設定されていることを確認する
エラーが表示されないFormMessage がないFormMessage コンポーネントを追加する
送信ボタンが常に無効になっているisPending の確認が誤っているformState.isSubmitting を使用する
値がリセットされないreset() を呼び出していない成功後に form.reset() を呼び出す
非同期バリデーションの頻度が高すぎるデバウンスされていないバリデーション関数をデバウンスする

リファレンスドキュメント

  • React Hook Form
  • Zod
  • MCP: mcp__documentation__fetch_docs → technology: react、topic: forms

コメント