Write and refactor React forms using react-hook-form with Zod validation. Use when creating new form components, converting existing forms to react-hook-form, or implementing form validation patterns.
Write and refactor React forms using react-hook-form with Zod validation. Use when creating new form components, converting existing forms to react-hook-form, or implementing form validation patterns.
React Hook Form Writer
This skill helps you write new forms and refactor existing forms to use react-hook-form following project best practices.
When to Use
Creating new form components from scratch
Converting existing forms to react-hook-form
Adding validation to forms
Implementing complex form patterns (nested forms, field arrays, multi-step)
Core Principles
1. Always Use Zod for Validation
Define schemas with Zod and integrate via zodResolver:
import { z } from"zod";
import { zodResolver } from"@hookform/resolvers/zod";
const formSchema = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Invalid email address"),
age: z.number().min(18, "Must be at least 18"),
});
typeFormValues = z.infer<typeof formSchema>;
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
name: "",
email: "",
age: 18,
},
});
2. Prefer useController Over Controller
Use useController hook for better composability in custom field components:
Leverage react-hook-form's uncontrolled approach for native inputs:
// Good: Uncontrolled with register
<input {...register("name")} />
// Only use Controller/useController for third-party controlled components// (e.g., shadcn Select, custom date pickers, rich text editors)
4. Use field.onChange for User Interactions, setValue for Programmatic Updates
When working with useController, use field.onChange for user interactions:
// Good: field.onChange for user interactionsconst { field } = useController({ name: "status", control });
<SelectonValueChange={field.onChange}value={field.value}>
{options.map((opt) => (
<SelectItemkey={opt.value}value={opt.value}>
{opt.label}
</SelectItem>
))}
</Select>// Bad: setValue for user interactions (breaks controller lifecycle)<SelectonValueChange={(v) => setValue("status", v)} value={watch("status")}>
Use setValue (from useFormContext) for programmatic updates in effects:
CRITICAL: Never use field.onChange inside useEffect dependencies
useController returns new field objects on every render. Including them in useEffect dependencies while also calling field.onChange() inside the effect causes infinite loops:
// BAD: Infinite loop - field objects change every renderconst { field } = useController({ name: "status" });
useEffect(() => {
field.onChange(defaultValue); // Triggers re-render
}, [field, defaultValue]); // field changes → effect runs → onChange → re-render → repeat// GOOD: Use setValue (stable) for programmatic updates in effectsconst { setValue } = useFormContext();
const { field } = useController({ name: "status" });
useEffect(() => {
setValue("status", defaultValue); // setValue is stable
}, [defaultValue, setValue]);
// User interactions still use field.onChangeconsthandleSelect = (value: string) => {
field.onChange(value);
};
Summary:
field.onChange → user interaction handlers (onClick, onSelect, etc.)
setValue → programmatic updates in useEffect or callbacks based on external data
5. Always Provide Default Values
Always provide defaultValues in useForm for all fields: