mcpField
R
react-hook-form

react-hook-form

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

react-hook-form について

react-hook-formは、React Hook FormとZod、shadcn/uiのフォームコンポーネントを扱うコーディングスキルです。セットアップ、複雑なバリデーション、フィールド例、よくある問題への対処を示します。

主な機能

  1. 01React Hook FormとZodのセットアップ
  2. 02複雑なバリデーションスキーマの作成
  3. 03shadcn/uiフォームコンポーネントの使用
  4. 04セレクトと日付ピッカーの処理
  5. 05リセット・送信状態・型の問題を調査

活用シーン

  1. 01メールとパスワードを検証するログインフォームの作成
  2. 02ユーザープロフィール項目の制約定義
  3. 03フォームへのセレクトや日付項目の追加
  4. 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>
  );
}

主要なパターン

パターン説明
zodResolverZod バリデーションの統合
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 がインターフェースと一致していることを確認してください

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

コメント