> react-hook-form
Build performant forms in React with React Hook Form. Use when a user asks to handle form validation, build complex multi-step forms, integrate forms with Zod schemas, or reduce form re-renders in React.
curl "https://skillshub.wtf/TerminalSkills/skills/react-hook-form?format=md"React Hook Form
Overview
React Hook Form is a performant form library that minimizes re-renders. Unlike controlled components (which re-render on every keystroke), RHF uses uncontrolled inputs and only re-renders when necessary. Integrates with Zod for schema validation.
Instructions
Step 1: Basic Form with Zod
npm install react-hook-form @hookform/resolvers zod
// components/SignupForm.tsx — Form with Zod validation
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
const signupSchema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters'),
email: z.string().email('Invalid email address'),
password: z.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Must contain an uppercase letter')
.regex(/[0-9]/, 'Must contain a number'),
confirmPassword: z.string(),
}).refine(data => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ['confirmPassword'],
})
type SignupInput = z.infer<typeof signupSchema>
export function SignupForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<SignupInput>({
resolver: zodResolver(signupSchema),
})
const onSubmit = async (data: SignupInput) => {
const res = await fetch('/api/auth/signup', {
method: 'POST',
body: JSON.stringify(data),
})
if (!res.ok) throw new Error('Signup failed')
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label htmlFor="name">Name</label>
<input {...register('name')} />
{errors.name && <p className="error">{errors.name.message}</p>}
</div>
<div>
<label htmlFor="email">Email</label>
<input type="email" {...register('email')} />
{errors.email && <p className="error">{errors.email.message}</p>}
</div>
<div>
<label htmlFor="password">Password</label>
<input type="password" {...register('password')} />
{errors.password && <p className="error">{errors.password.message}</p>}
</div>
<div>
<label htmlFor="confirmPassword">Confirm Password</label>
<input type="password" {...register('confirmPassword')} />
{errors.confirmPassword && <p className="error">{errors.confirmPassword.message}</p>}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Creating account...' : 'Sign Up'}
</button>
</form>
)
}
Step 2: Dynamic Fields
// components/InvoiceForm.tsx — Dynamic line items
import { useForm, useFieldArray } from 'react-hook-form'
interface InvoiceForm {
clientName: string
items: Array<{ description: string; quantity: number; price: number }>
}
export function InvoiceForm() {
const { register, control, handleSubmit, watch } = useForm<InvoiceForm>({
defaultValues: { items: [{ description: '', quantity: 1, price: 0 }] },
})
const { fields, append, remove } = useFieldArray({ control, name: 'items' })
const items = watch('items')
const total = items.reduce((sum, item) => sum + item.quantity * item.price, 0)
return (
<form onSubmit={handleSubmit(console.log)}>
<input {...register('clientName')} placeholder="Client name" />
{fields.map((field, index) => (
<div key={field.id} className="flex gap-2">
<input {...register(`items.${index}.description`)} placeholder="Description" />
<input type="number" {...register(`items.${index}.quantity`, { valueAsNumber: true })} />
<input type="number" {...register(`items.${index}.price`, { valueAsNumber: true })} step="0.01" />
<button type="button" onClick={() => remove(index)}>×</button>
</div>
))}
<button type="button" onClick={() => append({ description: '', quantity: 1, price: 0 })}>
Add Item
</button>
<p>Total: ${total.toFixed(2)}</p>
<button type="submit">Create Invoice</button>
</form>
)
}
Step 3: Server Actions (Next.js)
// app/settings/page.tsx — Server action with RHF
'use client'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { updateProfile } from './actions'
export function ProfileForm({ user }) {
const form = useForm({
resolver: zodResolver(profileSchema),
defaultValues: { name: user.name, bio: user.bio },
})
return (
<form action={async (formData) => {
const valid = await form.trigger()
if (!valid) return
await updateProfile(formData)
}}>
<input {...form.register('name')} />
<textarea {...form.register('bio')} />
<button type="submit">Save</button>
</form>
)
}
Guidelines
- Always use Zod resolver — share validation between frontend forms and API routes.
- Use
useFieldArrayfor dynamic lists (invoice items, team members, addresses). registeruses uncontrolled inputs — fastest performance, minimal re-renders.- Use
watchsparingly — it triggers re-renders. UseuseWatchfor isolated subscriptions. - For complex forms with many sections, use
FormProviderto pass form context without prop drilling.
> related_skills --same-repo
> zustand
You are an expert in Zustand, the small, fast, and scalable state management library for React. You help developers manage global state without boilerplate using Zustand's hook-based stores, selectors for performance, middleware (persist, devtools, immer), computed values, and async actions — replacing Redux complexity with a simple, un-opinionated API in under 1KB.
> zod
You are an expert in Zod, the TypeScript-first schema declaration and validation library. You help developers define schemas that validate data at runtime AND infer TypeScript types at compile time — eliminating the need to write types and validators separately. Used for API input validation, form validation, environment variables, config files, and any data boundary.
> xero-accounting
Integrate with the Xero accounting API to sync invoices, expenses, bank transactions, and contacts — and generate financial reports like P&L and balance sheet. Use when: connecting apps to Xero, automating bookkeeping workflows, syncing accounting data, or pulling financial reports programmatically.
> windsurf-rules
Configure Windsurf AI coding assistant with .windsurfrules and workspace rules. Use when: customizing Windsurf for a project, setting AI coding standards, creating team-shared Windsurf configurations, or tuning Cascade AI behavior.