Agent Skills: React Hook Form + Zod Validation

|

UncategorizedID: jezweb/claude-skills/react-hook-form-zod

Skill Files

Browse the full folder contents for react-hook-form-zod.

Download Skill

Loading file tree…

skills/react-hook-form-zod/SKILL.md

Skill Metadata

Name
react-hook-form-zod
Description
|

React Hook Form + Zod Validation

Status: Production Ready ✅ Last Verified: 2026-01-09 Latest Versions: react-hook-form@7.70.0, zod@4.3.5, @hookform/resolvers@5.2.2


Quick Start

npm install react-hook-form@7.70.0 zod@4.3.5 @hookform/resolvers@5.2.2

Basic Form Pattern:

const schema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
})

type FormData = z.infer<typeof schema>

const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
  resolver: zodResolver(schema),
  defaultValues: { email: '', password: '' }, // REQUIRED to prevent uncontrolled warnings
})

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

Server Validation (CRITICAL - never skip):

// SAME schema on server
const data = schema.parse(await req.json())

Key Patterns

useForm Options (validation modes):

  • mode: 'onSubmit' (default) - Best performance
  • mode: 'onBlur' - Good balance
  • mode: 'onChange' - Live feedback, more re-renders
  • shouldUnregister: true - Remove field data when unmounted (use for multi-step forms)

Zod Refinements (cross-field validation):

z.object({ password: z.string(), confirm: z.string() })
  .refine((data) => data.password === data.confirm, {
    message: "Passwords don't match",
    path: ['confirm'], // CRITICAL: Error appears on this field
  })

Zod Transforms:

z.string().transform((val) => val.toLowerCase()) // Data manipulation
z.string().transform(parseInt).refine((v) => v > 0) // Chain with refine

zodResolver connects Zod to React Hook Form, preserving type safety


Registration

register (for standard HTML inputs):

<input {...register('email')} /> // Uncontrolled, best performance

Controller (for third-party components):

<Controller
  name="category"
  control={control}
  render={({ field }) => <CustomSelect {...field} />} // MUST spread {...field}
/>

When to use Controller: React Select, date pickers, custom components without ref. Otherwise use register.


Error Handling

Display errors:

{errors.email && <span role="alert">{errors.email.message}</span>}
{errors.address?.street?.message} // Nested errors (use optional chaining)

Server errors:

const onSubmit = async (data) => {
  const res = await fetch('/api/submit', { method: 'POST', body: JSON.stringify(data) })
  if (!res.ok) {
    const { errors: serverErrors } = await res.json()
    Object.entries(serverErrors).forEach(([field, msg]) => setError(field, { message: msg }))
  }
}

Advanced Patterns

useFieldArray (dynamic lists):

const { fields, append, remove } = useFieldArray({ control, name: 'contacts' })

{fields.map((field, index) => (
  <div key={field.id}> {/* CRITICAL: Use field.id, NOT index */}
    <input {...register(`contacts.${index}.name` as const)} />
    {errors.contacts?.[index]?.name && <span>{errors.contacts[index].name.message}</span>}
    <button onClick={() => remove(index)}>Remove</button>
  </div>
))}
<button onClick={() => append({ name: '', email: '' })}>Add</button>

Async Validation (debounce):

const debouncedValidation = useDebouncedCallback(() => trigger('username'), 500)

Multi-Step Forms:

const step1 = z.object({ name: z.string(), email: z.string().email() })
const step2 = z.object({ address: z.string() })
const fullSchema = step1.merge(step2)

const nextStep = async () => {
  const isValid = await trigger(['name', 'email']) // Validate specific fields
  if (isValid) setStep(2)
}

Conditional Validation:

z.discriminatedUnion('accountType', [
  z.object({ accountType: z.literal('personal'), name: z.string() }),
  z.object({ accountType: z.literal('business'), companyName: z.string() }),
])

shadcn/ui Integration

Note: shadcn/ui deprecated the Form component. Use the Field component for new implementations (check latest docs).

Legacy Form component:

<FormField control={form.control} name="username" render={({ field }) => (
  <FormItem>
    <FormControl><Input {...field} /></FormControl>
    <FormMessage />
  </FormItem>
)} />

Performance

  • Use register (uncontrolled) over Controller (controlled) for standard inputs
  • Use watch('email') not watch() (isolates re-renders to specific fields)
  • shouldUnregister: true for multi-step forms (clears data on unmount)

Critical Rules

Always set defaultValues (prevents uncontrolled→controlled warnings)

Validate on BOTH client and server (client can be bypassed - security!)

Use field.id as key in useFieldArray (not index)

Spread {...field} in Controller render

Use z.infer<typeof schema> for type inference

Never skip server validation (security vulnerability)

Never mutate values directly (use setValue())

Never mix controlled + uncontrolled patterns

Never use index as key in useFieldArray


Known Issues (12 Prevented)

  1. Zod v4 Type Inference - #13109 (Closed 2025-11-01): Use z.infer<typeof schema>. Resolved in v7.66.x+.

  2. Uncontrolled→Controlled Warning - Always set defaultValues for all fields

  3. Nested Object Errors - Use optional chaining: errors.address?.street?.message

  4. Array Field Re-renders - Use key={field.id} in useFieldArray (not index)

  5. Async Validation Race Conditions - Debounce validation, cancel pending requests

  6. Server Error Mapping - Use setError() to map server errors to fields

  7. Default Values Not Applied - Set defaultValues in useForm options (not useState)

  8. Controller Field Not Updating - Always spread {...field} in render function

  9. useFieldArray Key Warnings - Use field.id as key (not index)

  10. Schema Refinement Error Paths - Specify path in refinement: refine(..., { path: ['fieldName'] })

  11. Transform vs Preprocess - Use transform for output, preprocess for input

  12. Multiple Resolver Conflicts - Use single resolver (zodResolver), combine schemas if needed


Bundled Resources

Templates: basic-form.tsx, advanced-form.tsx, shadcn-form.tsx, server-validation.ts, async-validation.tsx, dynamic-fields.tsx, multi-step-form.tsx, package.json

References: zod-schemas-guide.md, rhf-api-reference.md, error-handling.md, performance-optimization.md, shadcn-integration.md, top-errors.md

Docs: https://react-hook-form.com/ | https://zod.dev/ | https://ui.shadcn.com/docs/components/form


License: MIT | Last Verified: 2026-01-09