--- name: react-hook-form-writer description: 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`: ```typescript 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"), }); type FormValues = z.infer; const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { name: "", email: "", age: 18, }, }); ``` ### 2. Prefer useController Over Controller Use `useController` hook for better composability in custom field components: ```typescript // Good: useController function TextField({ name, control, label }: TextFieldProps) { const { field, fieldState } = useController({ name, control }); return (
{fieldState.error && {fieldState.error.message}}
); } // Avoid: Controller component (less composable) } /> ``` ### 3. Uncontrolled by Default Leverage react-hook-form's uncontrolled approach for native inputs: ```typescript // Good: Uncontrolled with register // 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: ```typescript // Good: field.onChange for user interactions const { field } = useController({ name: "status", control }); // Bad: setValue for user interactions (breaks controller lifecycle) ``` ### 8. Use useFieldArray for Dynamic Lists ```typescript const { fields, append, remove } = useFieldArray({ control, name: "items", }); return (
{fields.map((field, index) => (
))}
); ``` ### 9. Proper Form Submission ```typescript const onSubmit = async (data: FormValues) => { try { await submitToApi(data); } catch (error) { // Handle API errors, optionally set form errors form.setError("root", { message: "Submission failed" }); } };
{/* fields */} {form.formState.errors.root && (
{form.formState.errors.root.message}
)}
``` ### 10. Reset Forms Correctly ```typescript // Good: Reset with new values form.reset({ name: "New Name", email: "new@email.com", }); // Good: Reset to default values form.reset(); // Bad: Manual field clearing setValue("name", ""); setValue("email", ""); ``` ### 11. Sub-form Validation with trigger() ```typescript // Validate specific fields (useful for multi-step forms) const isStepValid = await form.trigger(["name", "email"]); if (isStepValid) { goToNextStep(); } ``` ### 12. Error Display Pattern ```typescript // Access errors via formState.errors const { formState: { errors }, } = form;
{errors.email && ( {errors.email.message} )}
``` ## Complete Example ```typescript import { useForm, useController, useFieldArray } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; const schema = z.object({ name: z.string().min(1, "Name is required"), email: z.string().email("Invalid email"), role: z.enum(["admin", "user", "guest"]), tags: z.array(z.object({ value: z.string().min(1) })), }); type FormValues = z.infer; function MyForm() { const form = useForm({ resolver: zodResolver(schema), defaultValues: { name: "", email: "", role: "user", tags: [], }, }); const { fields, append, remove } = useFieldArray({ control: form.control, name: "tags", }); const onSubmit = async (data: FormValues) => { console.log(data); }; return (
{form.formState.errors.name && ( {form.formState.errors.name.message} )}
{form.formState.errors.email && ( {form.formState.errors.email.message} )}
{fields.map((field, index) => (
))}
); } // Custom controlled component using useController function RoleSelect({ control }: { control: Control }) { const { field, fieldState } = useController({ name: "role", control, }); return (
{fieldState.error && {fieldState.error.message}}
); } ``` ## Refactoring Checklist When refactoring existing forms to react-hook-form: 1. [ ] Define Zod schema matching existing validation 2. [ ] Set up useForm with zodResolver and defaultValues 3. [ ] Replace controlled inputs with register() where possible 4. [ ] Use useController for third-party controlled components 5. [ ] Replace manual state management with form state 6. [ ] Convert submit handlers to use handleSubmit 7. [ ] Update error display to use formState.errors 8. [ ] Replace manual arrays with useFieldArray 9. [ ] Remove unnecessary useState for form values