react-hook-form について
react-hook-formは、React Hook FormとZod、shadcn/uiのフォームコンポーネントを扱うコーディングスキルです。セットアップ、複雑なバリデーション、フィールド例、よくある問題への対処を示します。
主な機能
- 01React Hook FormとZodのセットアップ
- 02複雑なバリデーションスキーマの作成
- 03shadcn/uiフォームコンポーネントの使用
- 04セレクトと日付ピッカーの処理
- 05リセット・送信状態・型の問題を調査
活用シーン
- 01メールとパスワードを検証するログインフォームの作成
- 02ユーザープロフィール項目の制約定義
- 03フォームへのセレクトや日付項目の追加
- 04デフォルト値や送信状態の問題を修正
MCP 対応
React Hook FormとZod、shadcn/uiの連携に関する詳細ドキュメントの取得先として、`mcp__documentation__fetch_docs`を案内しています。
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ claude-dev-suite/ claude-dev-suite --skill react-hook-formインストール後、Claude Code で/react-hook-formを入力して読み込まれたか確認します。
詳細
Zod を使用した React Hook Form
深い知識:React Hook Form と Zod、shadcn/ui の統合に関する包括的なドキュメントについては、technology:
react-hook-formを指定してmcp__documentation__fetch_docsを使用してください。
フォームのセットアップ
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const loginSchema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Password must be at least 8 characters'),
});
type LoginFormData = z.infer<typeof loginSchema>;
function LoginForm() {
const form = useForm<LoginFormData>({
resolver: zodResolver(loginSchema),
defaultValues: {
email: '',
password: '',
},
});
const onSubmit = async (data: LoginFormData) => {
await login(data);
};
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
{/* fields */}
</form>
);
}
shadcn/ui のフォームコンポーネントを使用する場合
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
function LoginForm() {
const form = useForm<LoginFormData>({
resolver: zodResolver(loginSchema),
defaultValues: { email: '', password: '' },
});
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="[email protected]" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? 'Loading...' : 'Login'}
</Button>
</form>
</Form>
);
}
複雑なバリデーションスキーマ
const userSchema = z.object({
name: z.string()
.min(2, 'Name must be at least 2 characters')
.max(100, 'Name must be less than 100 characters'),
email: z.string().email('Invalid email'),
role: z.enum(['admin', 'user', 'manager'], {
errorMap: () => ({ message: 'Select a valid role' }),
}),
department: z.string().optional(),
startDate: z.date({
required_error: 'Start date is required',
}),
salary: z.number()
.min(0, 'Salary must be positive')
.optional(),
});
// Conditional validation
const formSchema = z.object({
hasAddress: z.boolean(),
address: z.string().optional(),
}).refine(
(data) => !data.hasAddress || data.address,
{ message: 'Address is required', path: ['address'] }
);
セレクトフィールド
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
<FormField
control={form.control}
name="role"
render={({ field }) => (
<FormItem>
<FormLabel>Role</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select role" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="user">User</SelectItem>
<SelectItem value="manager">Manager</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
日付ピッカーフィールド
import { Calendar } from '@/components/ui/calendar';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { format } from 'date-fns';
import { CalendarIcon } from 'lucide-react';
<FormField
control={form.control}
name="startDate"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Start Date</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button variant="outline" className="w-full justify-start text-left">
<CalendarIcon className="mr-2 h-4 w-4" />
{field.value ? format(field.value, 'PPP') : 'Pick a date'}
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar
mode="single"
selected={field.value}
onSelect={field.onChange}
initialFocus
/>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
API ミューテーションを使用したフォーム
import { useMutation, useQueryClient } from '@tanstack/react-query';
function UserForm({ userId }: { userId?: string }) {
const queryClient = useQueryClient();
const isEditing = !!userId;
const mutation = useMutation({
mutationFn: isEditing ? usersApi.update : usersApi.create,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
toast.success(isEditing ? 'User updated' : 'User created');
},
onError: (error) => {
toast.error(error.message);
},
});
const onSubmit = (data: UserFormData) => {
if (isEditing) {
mutation.mutate({ id: userId, ...data });
} else {
mutation.mutate(data);
}
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
{/* fields */}
<Button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? 'Saving...' : 'Save'}
</Button>
</form>
</Form>
);
}
主要なパターン
| パターン | 説明 |
|---|---|
zodResolver | Zod バリデーションの統合 |
form.formState | フォームの状態(isSubmitting、errors など) |
form.reset() | フォームをデフォルト値にリセットします |
form.setValue() | 値をプログラムで設定します |
form.watch() | フィールドの変更を監視します |
form.trigger() | バリデーションを実行します |
このスキルを使用しない場合
- React 19 の Server Actions フォーム - useActionState パターンには
react-19スキルを使用してください - 基本的なフォーム処理 - 一般的なフォームパターンには
react-formsスキルを使用してください - shadcn/ui 以外のコンポーネント - パターンを適用するか、
react-formsスキルを使用してください - Zod 以外のバリデーション - その他のバリデーションライブラリには
react-formsスキルを使用してください
アンチパターン
| アンチパターン | 問題 | 解決策 |
|---|---|---|
| zodResolver を使用しない | 手動でのバリデーションや定型コードが必要になります | Zod では必ず zodResolver を使用してください |
| FormField ラッパーがない | エラー処理ができず、UX が低下します | 入力を FormField でラップしてください |
| formState.isSubmitting を確認しない | 送信中にボタンが無効になりません | isSubmitting を使ってボタンを無効にしてください |
| FormField を使わずに register を使用する | shadcn/ui と統合されません | Controller を使って FormField を使用してください |
| 成功後に reset() を呼び出さない | フォームがクリアされません | 送信が成功したら form.reset() を呼び出してください |
| コンポーネント内に複雑なバリデーションを記述する | テストや保守が困難になります | Zod でスキーマを定義してください |
| ミューテーションのエラーを処理しない | 失敗が通知されません | ミューテーションの onError を使用してください |
簡単なトラブルシューティング
| 問題 | 考えられる原因 | 対処方法 |
|---|---|---|
| バリデーションが動作しない | zodResolver が指定されていない | useForm に resolver: zodResolver(schema) を追加してください |
| エラーが表示されない | FormMessage がない | FormField に を追加してください |
| フォームがリセットされない | reset() を呼び出していない | 送信が成功した後に form.reset() を呼び出してください |
| 送信ボタンが無効にならない | isSubmitting を使用していない | form.formState.isSubmitting を使用してください |
| カスタムコンポーネントが動作しない | Controller を使用していない | FormField 内で Controller を使ってラップしてください |
| デフォルト値が表示されない | defaultValues に指定されていない | useForm の defaultValues に追加してください |
| 型エラーが発生する | スキーマが FormData 型と一致しない | z.infer がインターフェースと一致していることを確認してください |
リファレンスドキュメント
コメント
Markdown 対応