mcpField
R
react-hook-form

react-hook-form

github.com/claude-dev-suiteDevelopment4636 installsUpdated 3d ago
Repository

About react-hook-form

react-hook-form is a coding skill focused on React Hook Form with Zod and shadcn/ui form components. It provides setup, complex validation, field examples, and remedies for common form issues.

Key features

  1. 01Set up React Hook Form with Zod
  2. 02Build complex validation schemas
  3. 03Use shadcn/ui form components
  4. 04Handle select and date-picker fields
  5. 05Troubleshoot reset, submit-state, and type issues

Use cases

  1. 01Build a login form with email and password validation
  2. 02Define constraints for user profile fields
  3. 03Add select or date fields to a form
  4. 04Fix default-value or submit-state problems

MCP support

The skill recommends `mcp__documentation__fetch_docs` to retrieve detailed documentation on React Hook Form integration with Zod and shadcn/ui.

Install

Copy the command and run it in a terminal.

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

After installing, type/react-hook-formin Claude Code to confirm it loaded.

Details

From the project README

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

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 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 matches interface

Reference Documentation

Comments

Markdown supported