react-forms について
React 開発者向けのフォームパターンをまとめたスキルです。制御・非制御入力、React Hook Form、検証の実践例とトラブルシューティングを扱います。
主な機能
- 01制御・非制御フォーム
- 02React Hook Form のパターン
- 03フィールド配列と制御コンポーネント
- 04検証とアクセシビリティの実践
- 05送信時の問題の調査
活用シーン
- 01入力状態の管理方法の選択
- 02スキーマ検証の設定
- 03動的なフォーム項目の処理
- 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
コメント
Markdown 対応