react-hook-form-zod
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 mu
By ovachiever · 714 installs
npx skills add ovachiever/droid-tings --skill react-hook-form-zod
Source repository · Upstream listing
React Hook Form + Zod Validation
Status : Production Ready ✅
Last Updated : 2025 11 20
Dependencies : None (standalone)
Latest Versions : react hook form@7.66.1, zod@4.1.12, @hookform/resolvers@5.2.2
Quick Start (10 Minutes)
1. Install Packages
Why These Packages :
react hook form : Performant, flexible form library with minimal re renders
zod : TypeScript first schema validation with type inference
@hookform/resolvers : Adapter to connect Zod (and other validators) to React Hook Form
2. Create Your First Form
CRITICAL :
Always set defaultValues to prevent "uncontrolled to controlled" warnings
Use zodResolver(schema) to connect Zod validation
Type form with z.infer<typeof schema for full type safety
Validate on both client AND server (never trust client validation alone)
3. Add Server Side Validation
Why Server Validation :
Client validation can be bypassed (inspect element, Postman, curl)
Server validation is your security layer
Same Zod schema = single source of truth
Type safety across frontend and backend
Core Concepts
useForm Hook Anatomy
useForm Options :
Option Description Default
resolver Validation resolver (e.g., zodResolver) undefined
mode When to validate ('onSubmit', 'onChange', 'onBlur', 'all') 'onSubmit'
reValidateMode When to re validate after error 'onChange'
defaultValues Initial form values {}
shouldUnregister Unregister inputs when unmounted false
criteriaMode Return all errors or first error only 'firstError'
Form Validation Modes :
onSubmit Validate on submit (best performance, less responsive)
onChange Validate on every change (live feedback, more re renders)
onBlur Validate when field loses focus (good balance)
all Validate on submit, blur, and change (most responsive, highest cost)
Zod Schema Definition
Type Inference :
Zod Refinements (Custom Validation)
Zod Transforms (Data Manipulation)
zodResolver Integration
What zodResolver Does :
1. Takes your Zod schema
2. Converts it to a format React Hook Form understands
3. Provides validation function that runs on form submission
4. Maps Zod errors to React Hook Form error format
5. Preserves type safety with TypeScript inference
zodResolver Options :
Form Registration Patterns
Pattern 1: Simple Input Registration
What register() Returns :
Pattern 2: Controller (for Custom Components)
Use Controller when the input doesn't expose ref (like custom components, React Select, date pickers, etc.):
When to Use Controller :
✅ Third party UI libraries (React Select, Material UI, Ant Design, etc.)
✅ Custom components that don't expose ref
✅ Components that don't use onChange (like checkboxes with custom handlers)
✅ Need fine grained control over field behavior
When NOT to Use Controller :
❌ Standard HTML inputs (use register instead it's simpler and faster)
❌ When performance is critical (Controller adds minimal overhead)
Pattern 3: useController (Reusable Controlled Inputs)
Error Handling
Displaying Errors
Error Object Structure
Form Level Validation Errors
Server Errors Integration
Advanced Patterns
Dynamic Form Fields (useFieldArray)
useFieldArray API :
fields Array of field items with unique IDs
append(value) Add new item to end
prepend(value) Add new item to beginning
insert(index, value) Insert item at index
remove(index) Remove item at index
update(index, value) Update item at index
replace(values) Replace entire array
Async Validation with Debouncing
Multi Step Form (Wizard)
Conditional Validation
shadcn/ui Integration
Using Form Component (Legacy)
Note : shadcn/ui states "We are not actively developing the Form component anymore." They recommend using the Field component for new implementations.
Using Field Component (Recommended)
Check shadcn/ui documentation for the latest Field component API as it's the actively maintained approach.
Performance Optimization
Form Mode Strategies
Controlled vs Uncontrolled Inputs
Recommendation : Use register for standard inputs, Controller only when necessary (third party components, custom behavior).
Isolation with Controller
shouldUnregister Flag
When to use :
✅ Multi step forms where steps have different fields
✅ Conditional fields that should not persist
✅ Want to clear data when component unmounts
When NOT to use :
❌ Want to preserve form data when toggling visibility
❌ Navigating between form sections (tabs, accordions)
Accessibility Best Practices
ARIA Attributes
Error Announcements
Focus Management
Critical Rules
Always Do
✅ Set defaultValues to prevent "uncontrolled to controlled" warnings
✅ Use zodResolver for Zod integration
✅ Type forms with z.infer
✅ Validate on both client AND server
✅ Use formState.errors for error display
✅ Add ARIA attributes for accessibility
✅ Use field.id for useFieldArray keys
✅ Debounce async validation
Never Do
❌ Skip server side validation (security vulnerability!)
❌ Use Zod v4 without checking type inference
❌ Forget to spread {...field} in Controller
❌ Mutate form values directly
❌ Use inline validation without debouncing
❌ Mix controlled and uncontrolled inputs
❌ Use index as key in useFieldArray
❌ Forget defaultValues for all fields
Known Issues Prevention
This skill prevents 12 documented issues:
Issue 1: Zod v4 Type Inference Errors
Error : Type inference doesn't work correctly with Zod v4
Source : [GitHub Issue 13109](https://github.com/react hook form/react hook form/issues/13109) (Closed 2025 11 01)
Why It Happens : Zod v4 changed how types are inferred
Prevention : Use correct type patterns: type FormData = z.infer<typeof schema
Note : Resolved in react hook form v7.66.x+. Upgrade to latest version to avoid this issue.
Issue 2: Uncontrolled to Controlled Warning
Error : "A component is changing an uncontrolled input to be controlled"
Source : React documentation
Why It Happens : Not setting defaultValues causes undefined value transition
Prevention : Always set defaultValues for all fields
Issue 3: Nested Object Validation Errors
Error : Errors for nested fields don't display correctly
Source : Common React Hook Form issue
Why It Happens : Accessing nested errors incorrectly
Prevention : Use optional chaining: errors.address?.street?.message
Issue 4: Array Field Re renders
Error : Form re renders excessively with array fields
Source : Performance issue
Why It Happens : Not using field.id as key
Prevention : Use key={field.id} in useFieldArray map
Issue 5: Async Validation Race Conditions
Error : Multiple validation requests cause conflicting results
Source : Common async pattern issue
Why It Happens : No debouncing or request cancellation
Prevention : Debounce validation and cancel pending requests
Issue 6: Server Error Mapping
Error : Server validation errors don't map to form fields
Source : Integration issue
Why It Happens : Server error format doesn't match React Hook Form format
Prevention : Use setError() to map server errors to fields
Issue 7: Default Values Not Applied
Error : Form fields don't show default values
Source : Common mistake
Why It Happens : defaultValues set after form initialization
Prevention : Set defaultValues in useForm options, not useState
Issue 8: Controller Field Not Updating
Error : Custom component doesn't update when value changes
Source : Common Controller issue
Why It Happens : Not spreading {...field} in render function
Prevention : Always spread {...field} to custom component
Issue 9: useFieldArray Key Warnings
Error : React warning about duplicate keys in list
Source : React list rendering
Why It Happens : Using array index as key instead of field.id
Prevention : Use field.id: key={field.id}
Issue 10: Schema Refinement Error Paths
Error : Custom validation errors appear at wrong field
Source : Zod refinement behavior
Why It Happens : Not specifying path in refinement options
Prevention : Add path option: refine(..., { message: '...', path: ['fieldName'] })
Issue 11: Transform vs Preprocess Confusion
Error : Data transformation doesn't work as expected
Source : Zod API confusion
Why It Happens : Using wrong method for use case
Prevention : Use transform for output transformation, preprocess for input transformation
Issue 12: Multiple Resolver Conflicts
Error : Form validation doesn't work with multiple resolvers
Source : Configuration error
Why It Happens : Trying to use multiple validation libraries
Prevention : Use single resolver (zodResolver), combine schemas if needed
Templates
See the templates/ directory for working examples:
1. basic form.tsx Simple login/signup form
2. advanced form.tsx Nested objects, arrays, conditional fields
3. shadcn form.tsx shadcn/ui Form component integration
4. server validation.ts Server side validation with same schema
5. async validation.tsx Async validation with debouncing
6. dynamic fields.tsx useFieldArray for adding/removing items
7. multi step form.tsx Wizard with per step validation
8. custom error display.tsx Custom error formatting
9. package.json Complete dependencies
References
See the references/ directory for deep dive documentation:
1. zod schemas guide.md Comprehensive Zod schema patterns
2. rhf api reference.md Complete React Hook Form API
3. error handling.md Error messages, formatting, accessibility
4. accessibility.md WCAG compliance, ARIA attributes
5. performance optimization.md Form modes, validation strategies
6. shadcn integration.md shadcn/ui Form vs Field components
7. top errors.md 12 common errors with solutions
8. links to official docs.md Organized documentation links
Official Documentation
React Hook Form : https://react hook form.com/
Zod : https://zod.dev/
@hookform/resolvers : https://github.com/react hook form/resolvers
shadcn/ui Form : https://ui.shadcn.com/docs/components/form
License : MIT
Last Verified : 2025 11 20
Maintainer : Jeremy Dawes (jeremy@jezweb.net)