Claude Code Plugins

Community-maintained marketplace

Feedback

react-hook-form-zod

@jezweb/claude-skills
91
0

|

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name react-hook-form-zod
description Build type-safe validated forms in React using React Hook Form and Zod schema validation. Single schema works on both client and server for DRY validation with full TypeScript type inference via z.infer. Use when: building forms with validation, integrating shadcn/ui Form components, implementing multi-step wizards, handling dynamic field arrays with useFieldArray, or fixing uncontrolled to controlled warnings, resolver errors, async validation issues.

React Hook Form + Zod Validation

Status: Production Ready ✅ Last Verified: 2025-11-28 Latest Versions: react-hook-form@7.66.1, zod@4.1.13, @hookform/resolvers@5.2.2


Quick Start

npm install react-hook-form@7.66.1 zod@4.1.13 @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: 2025-11-28