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
Pattern Description
zodResolver
Zod validation integration
form.formState
Form 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-Pattern Problem Solution
Not using zodResolver Manual validation, boilerplate Always use zodResolver with Zod
Missing FormField wrapper No error handling, poor UX Wrap inputs in FormField
Not checking formState.isSubmitting Button not disabled during submit Use isSubmitting to disable button
Using register without FormField No shadcn/ui integration Use FormField with Controller
Not calling reset() after success Form not cleared Call form.reset() on successful submit
Complex validation in component Hard to test, maintain Define schema with Zod
Not handling mutation errors Silent failures Use onError in mutation
Quick Troubleshooting
Issue Likely Cause Fix
Validation not working Missing zodResolver Add resolver: zodResolver(schema) to useForm
Errors not displaying Missing FormMessage Add in FormField
Form not resetting Not calling reset() Call form.reset() after successful submit
Submit button not disabling Not using isSubmitting Use form.formState.isSubmitting
Custom component not working Not using Controller Wrap with Controller in FormField
Default values not showing Not in defaultValues Add to defaultValues in useForm
Type errors Schema not matching FormData type Ensure z.infer matches interface
Reference Documentation
-
Form Patterns
-
Validation Schemas