react-hook-form

React Hook Form - Performant Form Management

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "react-hook-form" with this command: npx skills add dsantiagomj/dsmj-ai-toolkit/dsantiagomj-dsmj-ai-toolkit-react-hook-form

React Hook Form - Performant Form Management

Build performant, flexible forms with easy validation

When to Use

Use React Hook Form when you need:

  • Form validation with schema-based validation (Zod, Yup)

  • Performance - minimizes re-renders with uncontrolled inputs

  • Complex forms with field arrays, nested objects, or conditional fields

  • Developer experience - TypeScript support and minimal boilerplate

  • Integration with UI libraries like shadcn/ui, Material UI

Choose alternatives when:

  • Simple forms with 1-2 fields (native HTML might suffice)

  • You need full controlled inputs for every keystroke (use useState)

  • Building non-React forms

Critical Patterns

Pattern 1: Schema-First Validation

// ✅ Good: Define schema first, infer types const formSchema = z.object({ email: z.string().email('Invalid email'), age: z.number().min(18, 'Must be 18+'), role: z.enum(['admin', 'user']), });

type FormData = z.infer<typeof formSchema>;

const form = useForm<FormData>({ resolver: zodResolver(formSchema), defaultValues: { email: '', age: 0, role: 'user' }, });

// ❌ Bad: Manual type definitions, sync issues interface FormData { email: string; age: number; }

const form = useForm<FormData>();

const validate = (data: FormData) => { const errors: Record<string, string> = {}; if (!data.email.includes('@')) errors.email = 'Invalid'; return errors; };

Why: Schema-first ensures single source of truth, better type safety, and automatic validation.

Pattern 2: Proper Error Display

// ✅ Good: Check existence before displaying {errors.email && ( <span className="text-red-500 text-sm"> {errors.email.message} </span> )}

// ✅ Good: With shadcn/ui FormMessage handles it <FormField control={form.control} name="email" render={({ field }) => ( <FormItem> <FormLabel>Email</FormLabel> <FormControl> <Input {...field} /> </FormControl> <FormMessage /> {/* Automatically shows errors */} </FormItem> )} />

// ❌ Bad: No null check, crashes if no error <span>{errors.email.message}</span>

// ❌ Bad: Generic error message loses context {errors.email && <span>Error occurred</span>}

Why: Proper error checking prevents runtime errors; specific messages improve UX.

Pattern 3: Field Arrays for Dynamic Lists

For field arrays and dynamic forms, see references/advanced.md.

Pattern 4: Controlled vs Uncontrolled

// ✅ Good: Uncontrolled (default) - best performance <input {...register('email')} />

// ✅ Good: Controlled when you need the value reactively const email = watch('email');

useEffect(() => { console.log('Email changed:', email); }, [email]);

// ✅ Good: Controller for third-party components import { Controller } from 'react-hook-form';

<Controller name="date" control={form.control} render={({ field }) => ( <DatePicker selected={field.value} onChange={field.onChange} /> )} />

// ❌ Bad: Making all fields controlled unnecessarily const email = watch('email'); <input value={email} onChange={(e) => setValue('email', e.target.value)} />

// ❌ Bad: Third-party component without Controller <DatePicker {...register('date')} />

Why: Uncontrolled inputs minimize re-renders; use controlled only when necessary; Controller properly integrates third-party components.

Pattern 5: Form Submission with Loading States

// ✅ Good: Use isSubmitting, handle errors, show feedback const onSubmit = async (data: FormData) => { try { await api.createUser(data); toast.success('User created successfully'); form.reset(); } catch (error) { toast.error('Failed to create user'); // Optionally set form errors form.setError('root', { message: 'Server error occurred', }); } };

<form onSubmit={form.handleSubmit(onSubmit)}> {/* fields */} <button type="submit" disabled={form.formState.isSubmitting}

{form.formState.isSubmitting ? 'Creating...' : 'Create User'}

</button> </form>

// ❌ Bad: No loading state, no error handling const onSubmit = async (data: FormData) => { await api.createUser(data); };

<button type="submit">Submit</button>

// ❌ Bad: Manual loading state, out of sync const [loading, setLoading] = useState(false);

const onSubmit = async (data: FormData) => { setLoading(true); await api.createUser(data); setLoading(false); };

Why: Built-in isSubmitting syncs with form state; proper error handling improves reliability.

Anti-Patterns

Anti-Pattern 1: Not Using defaultValues

// ❌ Problem: Uncontrolled inputs without defaults cause issues const form = useForm<FormData>({ resolver: zodResolver(formSchema), // No defaultValues });

// Form state is undefined, causes bugs with reset, dirty checking

Why it's wrong: React Hook Form tracks changes from initial state; without defaults, isDirty , reset() , and validation may behave unexpectedly.

Solution:

// ✅ Always provide defaultValues const form = useForm<FormData>({ resolver: zodResolver(formSchema), defaultValues: { email: '', name: '', age: 0, role: 'user', }, });

// ✅ Or use async defaultValues for fetched data const form = useForm<FormData>({ resolver: zodResolver(formSchema), defaultValues: async () => { const user = await fetchUser(); return user; }, });

Anti-Pattern 2: Destructuring register

// ❌ Problem: Destructuring breaks the ref const { onChange, onBlur, name } = register('email'); <input onChange={onChange} onBlur={onBlur} name={name} />

// ❌ Problem: Spreading twice loses the ref <input {...form.register('email')} {...otherProps} />

Why it's wrong: register returns a ref that must be attached; destructuring or overriding loses the connection.

Solution:

// ✅ Spread register directly <input {...register('email')} />

// ✅ Add props before register spread <input placeholder="Email" {...register('email')} />

// ✅ For custom props that might conflict, use register options <input {...register('email', { onChange: (e) => { // custom logic }, })} />

Anti-Pattern 3: Validation in onChange

// ❌ Problem: Triggering validation on every keystroke <input {...register('email')} onChange={(e) => { form.trigger('email'); // Validates on every key }} />

Why it's wrong: Creates poor UX (errors show immediately) and performance issues.

Solution:

// ✅ Use mode configuration for validation timing const form = useForm<FormData>({ resolver: zodResolver(formSchema), mode: 'onBlur', // Validate on blur, not onChange reValidateMode: 'onChange', // Re-validate onChange after first error });

// ✅ Manual trigger only when needed (e.g., dependent fields) const password = watch('password');

useEffect(() => { if (form.formState.touchedFields.confirmPassword) { form.trigger('confirmPassword'); } }, [password]);

For more anti-patterns (setState vs setValue, form state, resetting forms), see references/advanced.md.

What This Skill Covers

  • Form registration and state management

  • Validation with Zod resolver

  • Field arrays for dynamic forms

  • Form submission and error handling

For advanced patterns, see references/.

Basic Form

'use client';

import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod';

const formSchema = z.object({ email: z.string().email('Invalid email address'), password: z.string().min(8, 'Password must be at least 8 characters'), });

type FormData = z.infer<typeof formSchema>;

export function LoginForm() { const { register, handleSubmit, formState: { errors, isSubmitting }, } = useForm<FormData>({ resolver: zodResolver(formSchema), });

const onSubmit = async (data: FormData) => { await fetch('/api/login', { method: 'POST', body: JSON.stringify(data), }); };

return ( <form onSubmit={handleSubmit(onSubmit)}> <input {...register('email')} /> {errors.email && <span>{errors.email.message}</span>}

  &#x3C;input type="password" {...register('password')} />
  {errors.password &#x26;&#x26; &#x3C;span>{errors.password.message}&#x3C;/span>}
  
  &#x3C;button type="submit" disabled={isSubmitting}>Login&#x3C;/button>
&#x3C;/form>

); }

With shadcn/ui

import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@/components/ui/form';

export function UserForm() { const form = useForm<FormData>({ resolver: zodResolver(formSchema), });

return ( <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)}> <FormField control={form.control} name="name" render={({ field }) => ( <FormItem> <FormLabel>Name</FormLabel> <FormControl> <Input {...field} /> </FormControl> <FormMessage /> </FormItem> )} /> </form> </Form> ); }

Quick Reference

// Create form const form = useForm<FormData>({ resolver: zodResolver(schema), defaultValues: { name: '' }, });

// Register input <input {...form.register('name')} />

// Handle submit form.handleSubmit(onSubmit)

// Get errors form.formState.errors.name?.message

// Set value form.setValue('name', 'John')

// Watch value const name = form.watch('name')

Learn More

  • Advanced Patterns: references/advanced.md - Field arrays, controlled components, complex validation

External References

  • React Hook Form Documentation

  • React Hook Form GitHub

Maintained by dsmj-ai-toolkit

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

patterns

No summary provided by upstream source.

Repository SourceNeeds Review
General

caching

No summary provided by upstream source.

Repository SourceNeeds Review
General

react

No summary provided by upstream source.

Repository SourceNeeds Review