MCPInstall

react-hook-form-zod

Build type-safe validated forms using React Hook Form v7 and Zod v4. Single schema works on client and server with full TypeScript inference via z.infer. Use when building forms, multi-step wizards, or fixing uncontrolled warnings, resolver errors, useFieldArray issues, performance problems with large forms.

Installs
1.2K
Last 24h
—
Trending rank
—
Security
Not audited

Install

Works with every agent the skills CLI supports.

npx skills add https://github.com/jezweb/claude-skills --skill react-hook-form-zod
  1. 01

    Install

    Run the command in your project. The CLI asks which agents to install for.

  2. 02

    Describe the task

    Ask your agent for something this skill covers. It loads react-hook-form-zod automatically when relevant.

  3. 03

    Review the result

    The agent follows the SKILL.md steps, scripts and references bundled with the skill.

SKILL.md

The instructions your agent reads when the skill is triggered.

React Hook Form + Zod Validation

Status: Production Ready ✅ Last Verified: 2026-01-20 Latest Versions: react-hook-form@7.71.1, 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

Zod v4.3.0+ Features:

// Exact optional (can omit field, but NOT undefined)
z.string().exactOptional()

// Exclusive union (exactly one must match)
z.xor([z.string(), z.number()])

// Import from JSON Schema
z.fromJSONSchema({ type: "object", properties: { name: { type: "string" } } })

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() }),
])

Conditional Fields with shouldUnregister:

const form = useForm({
  resolver: zodResolver(schema),
  shouldUnregister: false, // Keep values when fields unmount (default)
})

// Or use conditional schema validation:
z.object({
  showAddress: z.boolean(),
  address: z.string(),
}).refine((data) => {
  if (data.showAddress) {
    return data.address.length > 0;
  }
  return true;
}, {
  message: "Address is required",
  path: ["address"],
})

shadcn/ui Integration

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

Common Import Mistake: IDEs/AI may auto-import Form from "react-hook-form" instead of from shadcn. Always import:

// ✅ Correct:
import { useForm } from "react-hook-form";
import { Form, FormField, FormItem } from "@/components/ui/form"; // shadcn

// ❌ Wrong (auto-import mistake):
import { useForm, Form } from "react-hook-form";

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)

Large Forms (300+ Fields)

Warning: Forms with 300+ fields using a resolver (Zod/Yup) AND reading formState properties can freeze for 10-15 seconds during registration. (Issue #13129)

Performance Characteristics:

  • Clean (no resolver, no formState read): Almost immediate
  • With resolver only: Almost immediate
  • With formState read only: Almost immediate
  • With BOTH resolver + formState read: ~9.5 seconds for 300 fields

Workarounds:

  1. Avoid destructuring formState - Read properties inline only when needed:
// ❌ Slow with 300+ fields:
const { isDirty, isValid } = form.formState;

// ✅ Fast:
const handleSubmit = () => {
  if (!form.formState.isValid) return; // Read inline only when needed
};
  1. Use mode: "onSubmit" - Don't validate on every change:
const form = useForm({
  resolver: zodResolver(largeSchema),
  mode: "onSubmit", // Validate only on submit, not onChange
});
  1. Split into sub-forms - Multiple smaller forms with separate schemas:
// Instead of one 300-field form, use 5-6 forms with 50-60 fields each
const form1 = useForm({ resolver: zodResolver(schema1) }); // Fields 1-50
const form2 = useForm({ resolver: zodResolver(schema2) }); // Fields 51-100
  1. Lazy render fields - Use tabs/accordion to mount only visible fields:
// Only mount fields for active tab, reduces initial registration time
{activeTab === 'personal' && <PersonalInfoFields />}
{activeTab === 'address' && <AddressFields />}

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 (20 Prevented)

  1. Zod v4 Type Inference - #13109: Use z.infer<typeof schema> explicitly. Resolved in v7.66.x+. Note: @hookform/resolvers has TypeScript compatibility issues with Zod v4 (#813). Workaround: Use import { z } from 'zod/v3' or wait for resolver update.

  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

  13. Zod v4 Optional Fields Bug - #13102: Setting optional fields (.optional()) to empty string "" incorrectly triggers validation errors. Workarounds: Use .nullish(), .or(z.literal("")), or z.preprocess((val) => val === "" ? undefined : val, z.email().optional())

  14. useFieldArray Primitive Arrays Not Supported - #12570: Design limitation. useFieldArray only works with arrays of objects, not primitives like string[]. Workaround: Wrap primitives in objects: [{ value: "string" }] instead of ["string"]

  15. useFieldArray SSR ID Mismatch - #12782: Hydration mismatch warnings with SSR (Remix, Next.js). Field IDs generated on server don't match client. Workaround: Use client-only rendering for field arrays or wait for V8 (uses deterministic key)

  16. Next.js 16 reset() Validation Bug - #13110: Calling form.reset() after Server Actions submission causes validation errors on next submit. Fixed in v7.65.0+. Before fix: Use setValue() instead of reset()

  17. Validation Race Condition - #13156: During resolver validation, intermediate render where isValidating=false but errors not populated yet. Don't derive validity from errors alone. Use: !errors.field && !isValidating

  18. ZodError Thrown in Beta Versions - #12816: Zod v4 beta versions throw ZodError directly instead of capturing in formState.errors. Fixed in stable Zod v4.1.x+. Avoid beta versions

  19. Large Form Performance - #13129: 300+ fields with resolver + formState read freezes for 10-15 seconds. See Performance section for 4 workarounds

  20. shadcn Form Import Confusion - IDEs/AI may auto-import Form from "react-hook-form" instead of shadcn. Always import Form components from @/components/ui/form


Upcoming Changes in V8 (Beta)

React Hook Form v8 (currently in beta as of v8.0.0-beta.1, released 2026-01-11) introduces breaking changes. RFC Discussion #7433

Breaking Changes:

  1. useFieldArray: id → key:
// V7:
const { fields } = useFieldArray({ control, name: "items" });
fields.map(field => <div key={field.id}>...</div>)

// V8:
const { fields } = useFieldArray({ control, name: "items" });
fields.map(field => <div key={field.key}>...</div>)
// keyName prop removed
  1. Watch component: names → name:
// V7:
<Watch names={["email", "password"]} />

// V8:
<Watch name={["email", "password"]} />
  1. watch() callback API removed:
// V7:
watch((data, { name, type }) => {
  console.log(data, name, type);
});

// V8: Use useWatch or manual subscription
const data = useWatch({ control });
useEffect(() => {
  console.log(data);
}, [data]);
  1. setValue() no longer updates useFieldArray:
// V7:
setValue("items", newArray); // Updates field array

// V8: Must use replace() API
const { replace } = useFieldArray({ control, name: "items" });
replace(newArray);

V8 Benefits:

  • Fixes SSR hydration mismatch (deterministic key instead of random id)
  • Improved performance
  • Better TypeScript inference

Migration Timeline: V8 is in beta. Stable release date TBD. Monitor releases for stable version.


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-20 | Skill Version: 2.1.0 | Changes: Added 8 new known issues (Zod v4 optional fields bug, useFieldArray primitives limitation, SSR hydration mismatch, performance guidance for large forms, Next.js 16 reset() bug, validation race condition, ZodError thrown in beta, shadcn import confusion), added Zod v4.3.0 features (.exactOptional(), .xor(), z.fromJSONSchema()), added conditional field patterns with shouldUnregister, added V8 beta breaking changes section, expanded Zod v4 resolver compatibility notes, updated to react-hook-form@7.71.1

FAQ

What does the react-hook-form-zod skill do?

Build type-safe validated forms using React Hook Form v7 and Zod v4. Single schema works on client and server with full TypeScript inference via z.infer. Use when building forms, multi-step wizards, or fixing uncontrolled warnings, resolver errors, useFieldArray issues, performance problems with large forms.

How do I install react-hook-form-zod?

Run npx skills add https://github.com/jezweb/claude-skills --skill react-hook-form-zod in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/react-hook-form-zod/ (personal) or .claude/skills/react-hook-form-zod/ (project).

Which AI agents can use react-hook-form-zod?

Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.

Is react-hook-form-zod safe to install?

No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.

Who maintains react-hook-form-zod?

It is published by jezweb in jezweb/claude-skills. It has 1,226 recorded installs.

More from jezweb/claude-skills

View all →

shadcn-ui

jezweb/claude-skills

Install and configure shadcn/ui components for React projects. Guides component selection, installation order, dependency management, customisation with semantic tokens, and common UI recipes (forms, data tables, navigation, modals). Use after tailwind-theme-builder has set up the theme infrastructure, when adding components, building forms, creating data tables, or setting up navigation.

Frontend & Design3.6K installs

color-palette

jezweb/claude-skills

Generate complete, accessible colour palettes from a single brand hex. Produces 11-shade scale (50-950), semantic tokens, dark mode variants, Tailwind v4 CSS output, WCAG contrast checks. Use whenever the user supplies a brand hex and asks for a palette, mentions setting up a design system, wants Tailwind theme colours from a brand colour, or asks to check colour accessibility / contrast.

Frontend & Design3.2K installs

tailwind-theme-builder

jezweb/claude-skills

Set up Tailwind v4 + shadcn/ui themed UI with dark mode. Install deps, configure CSS variables via @theme inline, wire dark mode toggle, verify. Use whenever the user mentions Tailwind v4, setting up Tailwind theming, shadcn/ui colours, dark mode, or troubleshooting colours not working, tw-animate-css errors, @theme inline conflicts, @apply breaking after upgrade, or v3 → v4 migration issues.

Frontend & Design3.2K installs

wordpress-elementor

jezweb/claude-skills

Edit Elementor pages and manage templates on WordPress sites via browser automation (for visual / structural changes) or WP-CLI (for safe text replacements). Use whenever the user wants to edit an Elementor page, update text in Elementor widgets, apply or manage Elementor templates, or make content changes to pages built with the Elementor page builder.

ux-audit

jezweb/claude-skills

Walk through a live web app AS a real user to find usability + behavioural bugs that static reviews miss. REQUIRES proof of interaction (typing, clicking, sending, observing) before any verdict — a sweep that didn't interact terminates with verdict 'Incomplete'. Walks threads, exercises every element, runs the multi-pane stress matrix, visual polish sweep, component perfection checklist, automated a11y (axe-core), pragmatic performance budget (LCP/CLS/INP), scenario battery (11 scenarios), and stress recipes including the real-flavour data battery. Hard gates: console errors/warnings = 0, network 5xx = 0, layout collapse = 0, axe Critical/Serious = 0, perf budget green. Audit-the-audit meta-check rejects rushed reports. Each finding has reproduction steps, evidence path, and suspected code location. Trigger with 'ux audit', 'walkthrough', 'qa sweep', 'audit the app', 'dogfood this', 'check all pages', 'find what's broken', 'stress the UI'.

Frontend & Design2.8K installs

tailwind-v4-shadcn

jezweb/claude-skills

Set up Tailwind v4 with shadcn/ui using @theme inline pattern and CSS variable architecture. Four-step pattern: CSS variables, Tailwind mapping, base styles, automatic dark mode. Prevents 8 documented errors. Use when initializing React projects with Tailwind v4, or fixing colors not working, tw-animate-css errors, @theme inline dark mode conflicts, @apply breaking, v3 migration issues.

Frontend & Design2.6K installs

Popular frontend & design skills

View category →

setup-matt-pocock-skills

mattpocock/skills

Configure this repo for the engineering skills: set up its issue tracker, triage label vocabulary, and domain doc layout. Run once before first use of the other engineering skills.

Frontend & Design1.1M installs

frontend-design

anthropics/skills

Guidance for distinctive, intentional visual design when building new UI or reshaping an existing one. Helps with aesthetic direction, typography, and making choices that don't read as templated defaults.

OfficialFrontend & Design1M installs

hyperframes-animation

heygen-com/hyperframes

All animation knowledge for HyperFrames — atomic motion rules, multi-phase scene blueprints, scene transitions, broader motion-design techniques, AND the seven runtime adapters (GSAP default, plus Lottie, Three.js, Anime.js, CSS keyframes, Web Animations API, TypeGPU). Use for any motion or animation task: pick 2-4 rules and compose, or load a blueprint, or look up runtime-specific API (e.g. GSAP eases / Lottie player / Three.js mixer). Also covers auditing an existing composition's choreography (animation map) and 24 named text-animation effects. HyperFrames-native: single paused timeline, seek-safe, deterministic.

prototype

mattpocock/skills

Build a throwaway prototype to answer a design question. Use when the user wants to sanity-check whether a state model or logic feels right, or explore what a UI should look like.

Frontend & Design999.2K installs

hyperframes-core

heygen-com/hyperframes

The HyperFrames composition contract — build one renderable project. Use for composition structure, the data- timing attributes, class="clip", tracks, sub-compositions, variables, framework-owned media playback, deterministic-render rules, and validation. Read before writing composition HTML.

Frontend & Design930.5K installs

vercel-react-best-practices

vercel-labs/agent-skills

React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.

OfficialFrontend & Design843.1K installs