react-hook-form

v2026.09.24

React Hook Form with Zod validation. Covers form setup, validation schemas, field components, error handling, and submission with shadcn/ui integration. USE WHEN: user mentions "React Hook Form", "RHF", "Zod validation", "shadcn/ui forms", "zodResolver", "FormField", asks about "form validation with Zod", "shadcn form components" DO NOT USE FOR: React 19 Server Actions - use `react-19` skill instead, basic form handling - use `react-forms` skill instead, controlled/uncontrolled patterns - use `react-forms` skill instead

GitHub
安装命令
npx skhub add claude-dev-suite/react-hook-form
Markdown
SKILL.md

React Hook Form with Zod

Deep Knowledge: Use mcp__documentation__fetch_docs with technology: react-hook-form for comprehensive documentation on React Hook Form integration with Zod and shadcn/ui.

Form Setup

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>
  );
}

With shadcn/ui Form Components

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@example.com" {...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>
  );
}

Complex Validation Schema

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'] }
);

Select Field

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>
  )}
/>

Date Picker Field

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>
  )}
/>

Form with API Mutation

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>
  );
}

Key Patterns

PatternDescription
zodResolverZod validation integration
form.formStateForm state (isSubmitting, errors, etc.)
form.reset()Reset form to defaults
form.setValue()Programmatic value setting
form.watch()Watch field changes
form.trigger()Trigger validation

When NOT to Use This Skill

  • React 19 Server Actions forms - Use react-19 skill for useActionState patterns
  • Basic form handling - Use react-forms skill for general form patterns
  • Non-shadcn/ui components - Adapt patterns or use react-forms skill
  • Non-Zod validation - Use react-forms skill for other validation libraries

Anti-Patterns

Anti-PatternProblemSolution
Not using zodResolverManual validation, boilerplateAlways use zodResolver with Zod
Missing FormField wrapperNo error handling, poor UXWrap inputs in FormField
Not checking formState.isSubmittingButton not disabled during submitUse isSubmitting to disable button
Using register without FormFieldNo shadcn/ui integrationUse FormField with Controller
Not calling reset() after successForm not clearedCall form.reset() on successful submit
Complex validation in componentHard to test, maintainDefine schema with Zod
Not handling mutation errorsSilent failuresUse onError in mutation

Quick Troubleshooting

IssueLikely CauseFix
Validation not workingMissing zodResolverAdd resolver: zodResolver(schema) to useForm
Errors not displayingMissing FormMessageAdd <FormMessage /> in FormField
Form not resettingNot calling reset()Call form.reset() after successful submit
Submit button not disablingNot using isSubmittingUse form.formState.isSubmitting
Custom component not workingNot using ControllerWrap with Controller in FormField
Default values not showingNot in defaultValuesAdd to defaultValues in useForm
Type errorsSchema not matching FormData typeEnsure z.infer<typeof schema> matches interface

Reference Documentation

发现
标签

此技能尚未发布标签。

版本
最新版本元数据

版本

v2026.09.24

发布时间

Sep 24, 2026

分类

未分类

许可证

MIT

源路径

skills/frontend-frameworks/react-hook-form

默认分支

main

最新提交

9496306

Tree SHA

fe4e2f1