Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:25:27 +08:00
commit c3e8cbf0e4
22 changed files with 7157 additions and 0 deletions

359
references/accessibility.md Normal file
View File

@@ -0,0 +1,359 @@
# Accessibility (a11y) Best Practices
Complete guide for building accessible forms.
---
## WCAG Compliance
### Required Elements
1. **Labels** - Every input must have a label
2. **Error Messages** - Must be accessible to screen readers
3. **Focus Management** - Errors should be announced
4. **Keyboard Navigation** - Full keyboard support
---
## ARIA Attributes
### Essential ARIA
```typescript
<input
id="email"
type="email"
{...register('email')}
aria-invalid={errors.email ? 'true' : 'false'}
aria-describedby={errors.email ? 'email-error' : 'email-hint'}
aria-required="true"
/>
<span id="email-hint">We'll never share your email</span>
{errors.email && (
<span id="email-error" role="alert">
{errors.email.message}
</span>
)}
```
### Live Regions for Error Announcements
```typescript
{Object.keys(errors).length > 0 && (
<div role="alert" aria-live="assertive" aria-atomic="true">
Form has {Object.keys(errors).length} errors. Please review.
</div>
)}
```
---
## Focus Management
### Focus First Error
```typescript
import { useEffect, useRef } from 'react'
const firstErrorRef = useRef<HTMLInputElement>(null)
useEffect(() => {
if (Object.keys(errors).length > 0) {
firstErrorRef.current?.focus()
}
}, [errors])
// In JSX
<input
ref={Object.keys(errors)[0] === 'email' ? firstErrorRef : undefined}
{...register('email')}
/>
```
### Using setFocus
```typescript
const onSubmit = async (data) => {
try {
await submitData(data)
} catch (error) {
setFocus('email') // Focus field programmatically
}
}
```
---
## Label Association
### Explicit Labels
```typescript
<label htmlFor="email">Email Address</label>
<input id="email" {...register('email')} />
```
### aria-label (When Visual Label Not Possible)
```typescript
<input
{...register('search')}
aria-label="Search products"
placeholder="Search..."
/>
```
### aria-labelledby (Multiple Labels)
```typescript
<h3 id="billing-heading">Billing Address</h3>
<input
{...register('billingStreet')}
aria-labelledby="billing-heading billing-street-label"
/>
<span id="billing-street-label">Street</span>
```
---
## Required Fields
### Visual Indicator
```typescript
<label htmlFor="email">
Email <span aria-label="required">*</span>
</label>
<input
id="email"
{...register('email')}
aria-required="true"
required
/>
```
### Legend for Required Fields
```typescript
<p className="required-legend">
<span aria-label="required">*</span> Required field
</p>
```
---
## Error Messaging
### Accessible Error Pattern
```typescript
<div>
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
{...register('password')}
aria-invalid={errors.password ? 'true' : 'false'}
aria-describedby={errors.password ? 'password-error' : 'password-hint'}
/>
<span id="password-hint" className="hint">
Must be at least 8 characters
</span>
{errors.password && (
<span id="password-error" role="alert" className="error">
{errors.password.message}
</span>
)}
</div>
```
---
## Fieldsets and Legends
### Grouping Related Fields
```typescript
<fieldset>
<legend>Contact Information</legend>
<div>
<label htmlFor="firstName">First Name</label>
<input id="firstName" {...register('firstName')} />
</div>
<div>
<label htmlFor="lastName">Last Name</label>
<input id="lastName" {...register('lastName')} />
</div>
</fieldset>
```
### Radio Groups
```typescript
<fieldset>
<legend>Choose your plan</legend>
<div>
<input
id="plan-basic"
type="radio"
value="basic"
{...register('plan')}
/>
<label htmlFor="plan-basic">Basic</label>
</div>
<div>
<input
id="plan-pro"
type="radio"
value="pro"
{...register('plan')}
/>
<label htmlFor="plan-pro">Pro</label>
</div>
</fieldset>
```
---
## Keyboard Navigation
### Tab Order
```typescript
// Ensure logical tab order with tabindex (use sparingly)
<input {...register('email')} tabIndex={1} />
<input {...register('password')} tabIndex={2} />
<button type="submit" tabIndex={3}>Submit</button>
```
### Skip Links
```typescript
<a href="#main-form" className="skip-link">
Skip to form
</a>
<form id="main-form">
{/* ... */}
</form>
```
---
## Button Accessibility
### Submit Button States
```typescript
<button
type="submit"
disabled={isSubmitting}
aria-busy={isSubmitting ? 'true' : 'false'}
aria-live="polite"
>
{isSubmitting ? 'Submitting...' : 'Submit Form'}
</button>
```
### Icon Buttons
```typescript
<button type="button" aria-label="Remove item" onClick={remove}>
<TrashIcon aria-hidden="true" />
</button>
```
---
## Screen Reader Announcements
### Status Messages
```typescript
{isSubmitSuccessful && (
<div role="status" aria-live="polite">
Form submitted successfully!
</div>
)}
```
### Loading States
```typescript
{isSubmitting && (
<div role="status" aria-live="polite">
Submitting form, please wait...
</div>
)}
```
---
## Color Contrast
### WCAG AA Standards
- Normal text: 4.5:1 minimum
- Large text: 3:1 minimum
- UI components: 3:1 minimum
```css
/* Good contrast examples */
.error {
color: #c41e3a; /* Red */
background: #ffffff; /* White */
/* Contrast ratio: 5.77:1 ✓ */
}
.button {
color: #ffffff;
background: #0066cc;
/* Contrast ratio: 7.33:1 ✓ */
}
```
---
## Testing
### Automated Testing Tools
- **axe DevTools** - Browser extension
- **Lighthouse** - Chrome DevTools
- **WAVE** - Web accessibility evaluation tool
### Manual Testing
1. **Keyboard Navigation** - Tab through entire form
2. **Screen Reader** - Test with NVDA (Windows) or VoiceOver (Mac)
3. **Zoom** - Test at 200% zoom
4. **High Contrast** - Test in high contrast mode
---
## Accessibility Checklist
- [ ] All inputs have associated labels
- [ ] Required fields are marked with aria-required
- [ ] Error messages use role="alert"
- [ ] Errors have aria-describedby linking to error text
- [ ] Form has clear heading structure
- [ ] Keyboard navigation works completely
- [ ] Focus is managed appropriately
- [ ] Color is not the only indicator of errors
- [ ] Contrast ratios meet WCAG AA standards
- [ ] Screen reader testing completed
---
**Resources**:
- WCAG Guidelines: https://www.w3.org/WAI/WCAG21/quickref/
- React Hook Form a11y: https://react-hook-form.com/advanced-usage#AccessibilityA11y

View File

@@ -0,0 +1,255 @@
# Error Handling Guide
Complete guide for handling and displaying form errors.
---
## Error Display Patterns
### 1. Inline Errors (Recommended)
```typescript
<input {...register('email')} />
{errors.email && (
<span role="alert" className="text-red-600">
{errors.email.message}
</span>
)}
```
### 2. Error Summary (Accessibility Best Practice)
```typescript
{Object.keys(errors).length > 0 && (
<div role="alert" aria-live="assertive" className="error-summary">
<h3>Please fix the following errors:</h3>
<ul>
{Object.entries(errors).map(([field, error]) => (
<li key={field}>
<strong>{field}:</strong> {error.message}
</li>
))}
</ul>
</div>
)}
```
### 3. Toast Notifications
```typescript
const onError = (errors) => {
toast.error(`Please fix ${Object.keys(errors).length} errors`)
}
<form onSubmit={handleSubmit(onSubmit, onError)}>
```
---
## ARIA Attributes
### Required Attributes
```typescript
<input
{...register('email')}
aria-invalid={errors.email ? 'true' : 'false'}
aria-describedby={errors.email ? 'email-error' : undefined}
aria-required="true"
/>
{errors.email && (
<span id="email-error" role="alert">
{errors.email.message}
</span>
)}
```
---
## Custom Error Messages
### Method 1: In Zod Schema
```typescript
const schema = z.object({
email: z.string()
.min(1, 'Email is required')
.email('Please enter a valid email address'),
password: z.string()
.min(8, { message: 'Password must be at least 8 characters long' }),
})
```
### Method 2: Custom Error Map
```typescript
const customErrorMap: z.ZodErrorMap = (issue, ctx) => {
switch (issue.code) {
case z.ZodIssueCode.too_small:
return { message: `Must be at least ${issue.minimum} characters` }
case z.ZodIssueCode.invalid_string:
if (issue.validation === 'email') {
return { message: 'Please enter a valid email address' }
}
break
default:
return { message: ctx.defaultError }
}
}
z.setErrorMap(customErrorMap)
```
---
## Error Formatting
### Flatten Errors for Forms
```typescript
try {
schema.parse(data)
} catch (error) {
if (error instanceof z.ZodError) {
const formattedErrors = error.flatten().fieldErrors
// Result: { email: ['Invalid email'], password: ['Too short'] }
}
}
```
### Format Errors for Display
```typescript
const formatError = (error: FieldError): string => {
switch (error.type) {
case 'required':
return 'This field is required'
case 'min':
return `Minimum length is ${error.message}`
case 'pattern':
return 'Invalid format'
default:
return error.message || 'Invalid value'
}
}
```
---
## Server Error Integration
```typescript
const onSubmit = async (data) => {
try {
const response = await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify(data),
})
const result = await response.json()
if (!result.success && result.errors) {
// Map server errors to form fields
Object.entries(result.errors).forEach(([field, message]) => {
setError(field, {
type: 'server',
message: Array.isArray(message) ? message[0] : message,
})
})
}
} catch (error) {
// Network error
setError('root', {
type: 'server',
message: 'Unable to connect. Please try again.',
})
}
}
```
---
## Error Persistence
### Clear Errors on Input Change
```typescript
<input
{...register('email')}
onChange={(e) => {
register('email').onChange(e)
clearErrors('email') // Clear error when user starts typing
}}
/>
```
### Clear All Errors on Submit Success
```typescript
const onSubmit = async (data) => {
const success = await submitData(data)
if (success) {
reset() // Clears form and errors
}
}
```
---
## Internationalization (i18n)
```typescript
import { useTranslation } from 'react-i18next'
const { t } = useTranslation()
const schema = z.object({
email: z.string().email(t('errors.invalidEmail')),
password: z.string().min(8, t('errors.passwordTooShort')),
})
```
---
## Error Components
### Reusable Error Display
```typescript
function FormError({ error }: { error?: FieldError }) {
if (!error) return null
return (
<div role="alert" className="error">
<svg className="icon">...</svg>
<span>{error.message}</span>
</div>
)
}
// Usage
<FormError error={errors.email} />
```
### Field Group with Error
```typescript
function FieldGroup({ name, label, type = 'text', register, errors }) {
return (
<div className="field-group">
<label htmlFor={name}>{label}</label>
<input
id={name}
type={type}
{...register(name)}
aria-invalid={errors[name] ? 'true' : 'false'}
/>
{errors[name] && <FormError error={errors[name]} />}
</div>
)
}
```
---
**Official Docs**: https://react-hook-form.com/

View File

@@ -0,0 +1,197 @@
# Links to Official Documentation
Organized links to official documentation and resources.
---
## React Hook Form
### Core Documentation
- **Main Site**: https://react-hook-form.com/
- **Get Started**: https://react-hook-form.com/get-started
- **API Reference**: https://react-hook-form.com/api
- **TS Support**: https://react-hook-form.com/ts
### Hooks
- **useForm**: https://react-hook-form.com/api/useform
- **useController**: https://react-hook-form.com/api/usecontroller
- **useFieldArray**: https://react-hook-form.com/api/usefieldarray
- **useWatch**: https://react-hook-form.com/api/usewatch
- **useFormContext**: https://react-hook-form.com/api/useformcontext
- **useFormState**: https://react-hook-form.com/api/useformstate
- **Controller**: https://react-hook-form.com/api/controller
### Advanced Usage
- **Smart Form Component**: https://react-hook-form.com/advanced-usage#SmartFormComponent
- **Error Messages**: https://react-hook-form.com/advanced-usage#ErrorMessages
- **Accessibility**: https://react-hook-form.com/advanced-usage#AccessibilityA11y
- **Performance**: https://react-hook-form.com/advanced-usage#PerformanceOptimization
- **Schema Validation**: https://react-hook-form.com/advanced-usage#SchemaValidation
### Examples
- **Examples Library**: https://react-hook-form.com/form-builder
- **CodeSandbox Examples**: https://codesandbox.io/examples/package/react-hook-form
---
## Zod
### Core Documentation
- **Main Site**: https://zod.dev/
- **Installation**: https://zod.dev/#installation
- **Basic Usage**: https://zod.dev/basics
- **Primitives**: https://zod.dev/primitives
- **Coercion**: https://zod.dev/coercion
### Schema Types
- **Objects**: https://zod.dev/objects
- **Arrays**: https://zod.dev/arrays
- **Unions**: https://zod.dev/unions
- **Records**: https://zod.dev/records
- **Maps**: https://zod.dev/maps
- **Sets**: https://zod.dev/sets
- **Promises**: https://zod.dev/promises
### Validation
- **Refinements**: https://zod.dev/refinements
- **Transforms**: https://zod.dev/transforms
- **Preprocessing**: https://zod.dev/preprocessing
- **Pipes**: https://zod.dev/pipes
### Error Handling
- **Error Handling**: https://zod.dev/error-handling
- **Custom Error Messages**: https://zod.dev/error-handling#custom-error-messages
- **Error Formatting**: https://zod.dev/error-handling#formatting
### TypeScript
- **Type Inference**: https://zod.dev/type-inference
- **Type Helpers**: https://zod.dev/type-inference#type-helpers
---
## @hookform/resolvers
### Documentation
- **Main Docs**: https://github.com/react-hook-form/resolvers
- **zodResolver**: https://github.com/react-hook-form/resolvers#zod
- **All Resolvers**: https://github.com/react-hook-form/resolvers#api
### Installation
```bash
npm install @hookform/resolvers
```
---
## shadcn/ui
### Form Components
- **Form Component**: https://ui.shadcn.com/docs/components/form
- **Input**: https://ui.shadcn.com/docs/components/input
- **Textarea**: https://ui.shadcn.com/docs/components/textarea
- **Select**: https://ui.shadcn.com/docs/components/select
- **Checkbox**: https://ui.shadcn.com/docs/components/checkbox
- **Radio Group**: https://ui.shadcn.com/docs/components/radio-group
- **Switch**: https://ui.shadcn.com/docs/components/switch
- **Button**: https://ui.shadcn.com/docs/components/button
### Installation
- **Vite Setup**: https://ui.shadcn.com/docs/installation/vite
- **Next.js Setup**: https://ui.shadcn.com/docs/installation/next
- **CLI**: https://ui.shadcn.com/docs/cli
---
## TypeScript
### Documentation
- **Handbook**: https://www.typescriptlang.org/docs/handbook/intro.html
- **Type Inference**: https://www.typescriptlang.org/docs/handbook/type-inference.html
- **Generics**: https://www.typescriptlang.org/docs/handbook/2/generics.html
---
## Accessibility (WCAG)
### Guidelines
- **WCAG 2.1**: https://www.w3.org/WAI/WCAG21/quickref/
- **ARIA Authoring Practices**: https://www.w3.org/WAI/ARIA/apg/
- **Forms Best Practices**: https://www.w3.org/WAI/tutorials/forms/
---
## Community Resources
### React Hook Form
- **GitHub**: https://github.com/react-hook-form/react-hook-form
- **Discord**: https://discord.gg/yYv7GZ8
- **Stack Overflow**: https://stackoverflow.com/questions/tagged/react-hook-form
### Zod
- **GitHub**: https://github.com/colinhacks/zod
- **Discord**: https://discord.gg/RcG33DQJdf
- **Stack Overflow**: https://stackoverflow.com/questions/tagged/zod
---
## Video Tutorials
### React Hook Form
- **Official YouTube**: https://www.youtube.com/@bluebill1049
- **Traversy Media**: https://www.youtube.com/watch?v=bU_eq8qyjic
- **Web Dev Simplified**: https://www.youtube.com/watch?v=cc_xmawJ8Kg
### Zod
- **Matt Pocock**: https://www.youtube.com/watch?v=L6BE-U3oy80
- **Theo**: https://www.youtube.com/watch?v=AeQ3f4zmSMs
---
## Blog Posts & Articles
### React Hook Form
- **React Hook Form Best Practices**: https://react-hook-form.com/faqs
- **Performance Comparison**: https://react-hook-form.com/faqs#PerformanceofReactHookForm
### Zod
- **Total TypeScript**: https://www.totaltypescript.com/tutorials/zod
- **Zod Tutorial**: https://zod.dev/tutorials
---
## Package Managers
### npm
```bash
npm install react-hook-form zod @hookform/resolvers
```
### pnpm
```bash
pnpm add react-hook-form zod @hookform/resolvers
```
### yarn
```bash
yarn add react-hook-form zod @hookform/resolvers
```
---
## Version Information
**Latest Tested Versions** (as of 2025-10-23):
- react-hook-form: 7.65.0
- zod: 4.1.12
- @hookform/resolvers: 5.2.2
**Check for updates**:
```bash
npm view react-hook-form version
npm view zod version
npm view @hookform/resolvers version
```
---
**Last Updated**: 2025-10-23

View File

@@ -0,0 +1,355 @@
# Performance Optimization Guide
Strategies for optimizing React Hook Form performance.
---
## Form Validation Modes
### onSubmit (Best Performance)
```typescript
const form = useForm({
mode: 'onSubmit', // Validate only on submit
resolver: zodResolver(schema),
})
```
**Pros**: Minimal re-renders, best performance
**Cons**: No live feedback
### onBlur (Good Balance)
```typescript
const form = useForm({
mode: 'onBlur', // Validate when field loses focus
resolver: zodResolver(schema),
})
```
**Pros**: Good UX, reasonable performance
**Cons**: Some re-renders on blur
### onChange (Live Feedback)
```typescript
const form = useForm({
mode: 'onChange', // Validate on every change
resolver: zodResolver(schema),
})
```
**Pros**: Immediate feedback
**Cons**: Most re-renders, can be slow with complex validation
### all (Maximum Validation)
```typescript
const form = useForm({
mode: 'all', // Validate on blur, change, and submit
resolver: zodResolver(schema),
})
```
**Pros**: Most responsive
**Cons**: Highest performance cost
---
## Controlled vs Uncontrolled
### Uncontrolled (Faster)
```typescript
// Best performance - no React state
<input {...register('email')} />
```
### Controlled (More Control)
```typescript
// More React state = more re-renders
<Controller
control={control}
name="email"
render={({ field }) => <Input {...field} />}
/>
```
**Rule**: Use `register` by default, `Controller` only when necessary.
---
## watch() Optimization
### Watch Specific Fields
```typescript
// BAD - Watches all fields, re-renders on any change
const values = watch()
// GOOD - Watch only what you need
const email = watch('email')
const [email, password] = watch(['email', 'password'])
```
### useWatch for Isolation
```typescript
import { useWatch } from 'react-hook-form'
// Isolated component - only re-renders when email changes
function EmailDisplay() {
const email = useWatch({ control, name: 'email' })
return <div>{email}</div>
}
```
---
## Debouncing Validation
### Manual Debounce
```typescript
import { useDebouncedCallback } from 'use-debounce'
const debouncedValidation = useDebouncedCallback(
() => trigger('username'),
500 // Wait 500ms
)
<input
{...register('username')}
onChange={(e) => {
register('username').onChange(e)
debouncedValidation()
}}
/>
```
---
## shouldUnregister Flag
### Keep Data When Unmounting
```typescript
const form = useForm({
shouldUnregister: false, // Keep field data when unmounted
})
```
**Use When**:
- Multi-step forms
- Tabbed interfaces
- Conditional fields that should persist
### Clear Data When Unmounting
```typescript
const form = useForm({
shouldUnregister: true, // Remove field data when unmounted
})
```
**Use When**:
- Truly conditional fields
- Dynamic forms
- Want to clear data automatically
---
## useFieldArray Optimization
### Use field.id as Key
```typescript
// CRITICAL for performance
{fields.map((field) => (
<div key={field.id}> {/* Not index! */}
...
</div>
))}
```
### Avoid Unnecessary Re-renders
```typescript
// Extract field components
const FieldItem = React.memo(({ field, index, register, remove }) => (
<div>
<input {...register(`items.${index}.name`)} />
<button onClick={() => remove(index)}>Remove</button>
</div>
))
// Use memoized component
{fields.map((field, index) => (
<FieldItem
key={field.id}
field={field}
index={index}
register={register}
remove={remove}
/>
))}
```
---
## formState Optimization
### Subscribe to Specific Properties
```typescript
// BAD - Subscribes to all formState changes
const { formState } = useForm()
// GOOD - Subscribe only to what you need
const { isDirty, isValid } = useForm().formState
// BETTER - Use useFormState for isolation
import { useFormState } from 'react-hook-form'
const { isDirty } = useFormState({ control })
```
---
## Resolver Optimization
### Memoize Schema
```typescript
// BAD - New schema on every render
const form = useForm({
resolver: zodResolver(z.object({ email: z.string() })),
})
// GOOD - Schema defined outside component
const schema = z.object({ email: z.string() })
function Form() {
const form = useForm({
resolver: zodResolver(schema),
})
}
```
---
## Large Forms
### Split into Sections
```typescript
function PersonalInfoSection() {
const { register } = useFormContext()
return (
<div>
<input {...register('firstName')} />
<input {...register('lastName')} />
</div>
)
}
function ContactInfoSection() {
const { register } = useFormContext()
return (
<div>
<input {...register('email')} />
<input {...register('phone')} />
</div>
)
}
function LargeForm() {
const methods = useForm()
return (
<FormProvider {...methods}>
<form>
<PersonalInfoSection />
<ContactInfoSection />
</form>
</FormProvider>
)
}
```
### Virtualize Long Lists
```typescript
import { useVirtualizer } from '@tanstack/react-virtual'
function VirtualizedFieldArray() {
const { fields } = useFieldArray({ control, name: 'items' })
const parentRef = React.useRef(null)
const rowVirtualizer = useVirtualizer({
count: fields.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
})
return (
<div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
<div style={{ height: `${rowVirtualizer.getTotalSize()}px` }}>
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
const field = fields[virtualRow.index]
return (
<div key={field.id}>
<input {...register(`items.${virtualRow.index}.name`)} />
</div>
)
})}
</div>
</div>
)
}
```
---
## Performance Benchmarks
| Optimization | Before | After | Improvement |
|--------------|--------|-------|-------------|
| mode: onSubmit vs onChange | 100ms | 20ms | 80% |
| watch() all vs watch('field') | 50ms | 10ms | 80% |
| field.id vs index key | 200ms | 50ms | 75% |
| Memoized schema | 30ms | 5ms | 83% |
---
## Profiling
### React DevTools Profiler
1. Open React DevTools
2. Go to Profiler tab
3. Click Record
4. Interact with form
5. Stop recording
6. Analyze render times
### Performance.mark API
```typescript
const onSubmit = (data) => {
performance.mark('form-submit-start')
// Submit logic
performance.mark('form-submit-end')
performance.measure('form-submit', 'form-submit-start', 'form-submit-end')
const measures = performance.getEntriesByName('form-submit')
console.log('Submit time:', measures[0].duration, 'ms')
}
```
---
**Official Docs**: https://react-hook-form.com/advanced-usage#PerformanceOptimization

View File

@@ -0,0 +1,420 @@
# React Hook Form API Reference
Complete API reference for React Hook Form v7.65.0
---
## useForm Hook
```typescript
const {
register,
handleSubmit,
watch,
formState,
setValue,
getValues,
reset,
trigger,
control,
setError,
clearErrors,
setFocus,
} = useForm<FormData>(options)
```
### Options
| Option | Type | Description |
|--------|------|-------------|
| `resolver` | `Resolver` | Schema validation resolver (zodResolver, etc.) |
| `mode` | `'onSubmit' \| 'onChange' \| 'onBlur' \| 'all'` | When to validate (default: 'onSubmit') |
| `reValidateMode` | `'onChange' \| 'onBlur'` | When to re-validate after error |
| `defaultValues` | `object \| () => object \| Promise<object>` | Initial form values |
| `values` | `object` | Controlled form values |
| `resetOptions` | `object` | Options for reset behavior |
| `shouldUnregister` | `boolean` | Unregister fields when unmounted |
| `shouldFocusError` | `boolean` | Focus first error on submit |
| `criteriaMode` | `'firstError' \| 'all'` | Return first error or all |
| `delayError` | `number` | Delay error display (ms) |
---
## register
Register input and apply validation rules.
```typescript
<input {...register('fieldName', options)} />
```
**Options**:
- `required`: `boolean | string`
- `min`: `number | { value: number, message: string }`
- `max`: `number | { value: number, message: string }`
- `minLength`: `number | { value: number, message: string }`
- `maxLength`: `number | { value: number, message: string }`
- `pattern`: `RegExp | { value: RegExp, message: string }`
- `validate`: `(value) => boolean | string | object`
- `valueAsNumber`: `boolean`
- `valueAsDate`: `boolean`
- `disabled`: `boolean`
- `onChange`: `(e) => void`
- `onBlur`: `(e) => void`
---
## handleSubmit
Wraps your form submission handler.
```typescript
<form onSubmit={handleSubmit(onSubmit, onError)}>
function onSubmit(data: FormData) {
// Valid data
}
function onError(errors: FieldErrors) {
// Validation errors
}
```
---
## watch
Watch specified inputs and return their values.
```typescript
// Watch all fields
const values = watch()
// Watch specific field
const email = watch('email')
// Watch multiple fields
const [email, password] = watch(['email', 'password'])
// Watch with callback
useEffect(() => {
const subscription = watch((value, { name, type }) => {
console.log(value, name, type)
})
return () => subscription.unsubscribe()
}, [watch])
```
---
## formState
Form state object.
```typescript
const {
isDirty, // Form has been modified
dirtyFields, // Object of modified fields
touchedFields, // Object of touched fields
isSubmitted, // Form has been submitted
isSubmitSuccessful, // Last submission successful
isSubmitting, // Form is currently submitting
isValidating, // Form is validating
isValid, // Form is valid
errors, // Validation errors
submitCount, // Number of submissions
} = formState
```
---
## setValue
Set field value programmatically.
```typescript
setValue('fieldName', value, options)
// Options
{
shouldValidate: boolean, // Trigger validation
shouldDirty: boolean, // Mark as dirty
shouldTouch: boolean, // Mark as touched
}
```
---
## getValues
Get current form values.
```typescript
// Get all values
const values = getValues()
// Get specific field
const email = getValues('email')
// Get multiple fields
const [email, password] = getValues(['email', 'password'])
```
---
## reset
Reset form to default values.
```typescript
reset() // Reset to defaultValues
reset({ email: '', password: '' }) // Reset to specific values
reset(undefined, {
keepErrors: boolean,
keepDirty: boolean,
keepIsSubmitted: boolean,
keepTouched: boolean,
keepIsValid: boolean,
keepSubmitCount: boolean,
})
```
---
## trigger
Manually trigger validation.
```typescript
// Trigger all fields
await trigger()
// Trigger specific field
await trigger('email')
// Trigger multiple fields
await trigger(['email', 'password'])
```
---
## setError
Set field error manually.
```typescript
setError('fieldName', {
type: 'manual',
message: 'Error message',
})
// Root error (not tied to specific field)
setError('root', {
type: 'server',
message: 'Server error',
})
```
---
## clearErrors
Clear field errors.
```typescript
clearErrors() // Clear all errors
clearErrors('email') // Clear specific field
clearErrors(['email', 'password']) // Clear multiple fields
```
---
## setFocus
Focus on specific field.
```typescript
setFocus('fieldName', { shouldSelect: true })
```
---
## Controller
For controlled components (third-party UI libraries).
```typescript
import { Controller } from 'react-hook-form'
<Controller
name="fieldName"
control={control}
defaultValue=""
rules={{ required: true }}
render={({ field, fieldState, formState }) => (
<CustomInput
{...field}
error={fieldState.error}
/>
)}
/>
```
**render props**:
- `field`: `{ value, onChange, onBlur, ref, name }`
- `fieldState`: `{ invalid, isTouched, isDirty, error }`
- `formState`: Full form state
---
## useController
Hook version of Controller (for reusable components).
```typescript
import { useController } from 'react-hook-form'
function CustomInput({ name, control }) {
const {
field,
fieldState: { invalid, isTouched, isDirty, error },
formState: { touchedFields, dirtyFields }
} = useController({
name,
control,
rules: { required: true },
defaultValue: '',
})
return <input {...field} />
}
```
---
## useFieldArray
Manage dynamic field arrays.
```typescript
import { useFieldArray } from 'react-hook-form'
const { fields, append, prepend, remove, insert, update, replace } = useFieldArray({
control,
name: 'items',
keyName: 'id', // Default: 'id'
})
```
**Methods**:
- `append(value)` - Add to end
- `prepend(value)` - Add to beginning
- `insert(index, value)` - Insert at index
- `remove(index)` - Remove at index
- `update(index, value)` - Update at index
- `replace(values)` - Replace entire array
**Important**: Use `field.id` as key, not array index!
```typescript
{fields.map((field, index) => (
<div key={field.id}> {/* Use field.id! */}
<input {...register(`items.${index}.name`)} />
</div>
))}
```
---
## useWatch
Subscribe to input changes without re-rendering entire form.
```typescript
import { useWatch } from 'react-hook-form'
const email = useWatch({
control,
name: 'email',
defaultValue: '',
})
```
---
## useFormState
Subscribe to form state without re-rendering entire form.
```typescript
import { useFormState } from 'react-hook-form'
const { isDirty, isValid } = useFormState({ control })
```
---
## useFormContext
Access form context (for deeply nested components).
```typescript
import { useFormContext } from 'react-hook-form'
function NestedComponent() {
const { register, formState: { errors } } = useFormContext()
return <input {...register('email')} />
}
// Wrap form with FormProvider
import { FormProvider, useForm } from 'react-hook-form'
function App() {
const methods = useForm()
return (
<FormProvider {...methods}>
<form>
<NestedComponent />
</form>
</FormProvider>
)
}
```
---
## ErrorMessage
Helper component for displaying errors (from @hookform/error-message).
```typescript
import { ErrorMessage } from '@hookform/error-message'
<ErrorMessage
errors={errors}
name="email"
render={({ message }) => <span className="error">{message}</span>}
/>
```
---
## DevTool
Development tool for debugging (from @hookform/devtools).
```typescript
import { DevTool } from '@hookform/devtools'
<DevTool control={control} />
```
---
**Official Docs**: https://react-hook-form.com/

View File

@@ -0,0 +1,390 @@
# shadcn/ui Integration Guide
Complete guide for using shadcn/ui with React Hook Form + Zod.
---
## Form Component (Legacy)
**Status**: "Not actively developed" according to shadcn/ui documentation
**Recommendation**: Use Field component for new projects (coming soon)
### Installation
```bash
npx shadcn@latest add form
```
### Basic Usage
```typescript
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
const schema = z.object({
username: z.string().min(2),
})
function ProfileForm() {
const form = useForm<z.infer<typeof schema>>({
resolver: zodResolver(schema),
defaultValues: { username: '' },
})
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
Your public display name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
)
}
```
---
## Form Component Anatomy
### FormField
```typescript
<FormField
control={form.control} // Required
name="fieldName" // Required
render={({ field, fieldState, formState }) => (
// Your field component
)}
/>
```
### FormItem
Container for field, label, description, and message.
```typescript
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>Helper text</FormDescription>
<FormMessage />
</FormItem>
```
### FormControl
Wraps the actual input component.
```typescript
<FormControl>
<Input {...field} />
</FormControl>
```
### FormLabel
Accessible label with automatic linking to input.
```typescript
<FormLabel>Email Address</FormLabel>
```
### FormDescription
Helper text for the field.
```typescript
<FormDescription>
We'll never share your email.
</FormDescription>
```
### FormMessage
Displays validation errors.
```typescript
<FormMessage />
```
---
## Common Patterns
### Input Field
```typescript
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="you@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
```
### Textarea
```typescript
<FormField
control={form.control}
name="bio"
render={({ field }) => (
<FormItem>
<FormLabel>Bio</FormLabel>
<FormControl>
<Textarea {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
```
### Select
```typescript
<FormField
control={form.control}
name="role"
render={({ field }) => (
<FormItem>
<FormLabel>Role</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a role" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="user">User</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
```
### Checkbox
```typescript
<FormField
control={form.control}
name="newsletter"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>Subscribe to newsletter</FormLabel>
<FormDescription>
Receive email updates about new products.
</FormDescription>
</div>
</FormItem>
)}
/>
```
### Radio Group
```typescript
<FormField
control={form.control}
name="plan"
render={({ field }) => (
<FormItem className="space-y-3">
<FormLabel>Select a plan</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="free" />
</FormControl>
<FormLabel className="font-normal">Free</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="pro" />
</FormControl>
<FormLabel className="font-normal">Pro</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
```
### Switch
```typescript
<FormField
control={form.control}
name="notifications"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="text-base">
Email Notifications
</FormLabel>
<FormDescription>
Receive emails about your account activity.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
```
---
## Nested Objects
```typescript
const schema = z.object({
user: z.object({
name: z.string(),
email: z.string().email(),
}),
})
<FormField
control={form.control}
name="user.name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
```
---
## Arrays
```typescript
const { fields, append, remove } = useFieldArray({
control: form.control,
name: 'items',
})
{fields.map((field, index) => (
<FormField
key={field.id}
control={form.control}
name={`items.${index}.name`}
render={({ field }) => (
<FormItem>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
))}
```
---
## Custom Validation
```typescript
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
{/* Custom error styling */}
{errors.username && (
<div className="text-sm font-medium text-destructive">
{errors.username.message}
</div>
)}
</FormItem>
)}
/>
```
---
## Field Component (Future)
**Status**: Recommended for new implementations (in development)
Check official docs for latest: https://ui.shadcn.com/docs/components/form
---
## Tips
1. **Always spread {...field}** in FormControl
2. **Use Form component** for automatic ID generation
3. **FormMessage** automatically displays errors
4. **Combine with Zod** for type-safe validation
5. **Check documentation** - Form component is not actively developed
---
**Official Docs**:
- shadcn/ui Form: https://ui.shadcn.com/docs/components/form
- React Hook Form: https://react-hook-form.com/

381
references/top-errors.md Normal file
View File

@@ -0,0 +1,381 @@
# Top 12 Common Errors with Solutions
Complete reference for known issues and their solutions.
---
## 1. Zod v4 Type Inference Errors
**Error**: Type inference doesn't work correctly with Zod v4
**Symptoms**:
```typescript
// Types don't match expected structure
const schema = z.object({ name: z.string() })
type FormData = z.infer<typeof schema> // Type issues
```
**Source**: [GitHub Issue #13109](https://github.com/react-hook-form/react-hook-form/issues/13109) (Closed 2025-11-01)
**Note**: This issue was resolved in react-hook-form v7.66.x. Upgrade to v7.66.1+ to avoid this problem.
**Solution**:
```typescript
// Use correct Zod v4 patterns
const schema = z.object({ name: z.string() })
type FormData = z.infer<typeof schema>
// Explicitly type useForm if needed
const form = useForm<z.infer<typeof schema>>({
resolver: zodResolver(schema),
})
```
---
## 2. Uncontrolled to Controlled Warning
**Error**: "A component is changing an uncontrolled input to be controlled"
**Symptoms**:
```
Warning: A component is changing an uncontrolled input of type text to be controlled.
Input elements should not switch from uncontrolled to controlled (or vice versa).
```
**Cause**: Not setting defaultValues causes fields to be undefined initially
**Solution**:
```typescript
// BAD
const form = useForm()
// GOOD - Always set defaultValues
const form = useForm({
defaultValues: {
email: '',
password: '',
remember: false,
},
})
```
---
## 3. Nested Object Validation Errors
**Error**: Errors for nested fields don't display correctly
**Symptoms**:
```typescript
// errors.address.street is undefined even though validation failed
<span>{errors.address.street?.message}</span> // Shows nothing
```
**Solution**:
```typescript
// Use optional chaining for nested errors
{errors.address?.street && (
<span>{errors.address.street.message}</span>
)}
// OR check if errors.address exists first
{errors.address && errors.address.street && (
<span>{errors.address.street.message}</span>
)}
```
---
## 4. Array Field Re-renders
**Error**: Form re-renders excessively with useFieldArray
**Cause**: Using array index as key instead of field.id
**Solution**:
```typescript
// BAD
{fields.map((field, index) => (
<div key={index}> {/* Using index causes re-renders */}
...
</div>
))}
// GOOD
{fields.map((field) => (
<div key={field.id}> {/* Use field.id */}
...
</div>
))}
```
---
## 5. Async Validation Race Conditions
**Error**: Multiple validation requests cause conflicting results
**Symptoms**: Old validation results override new ones
**Solution**:
```typescript
// Use debouncing
import { useDebouncedCallback } from 'use-debounce'
const debouncedValidation = useDebouncedCallback(
() => trigger('username'),
500 // Wait 500ms after user stops typing
)
// AND cancel pending requests
const abortControllerRef = useRef<AbortController | null>(null)
useEffect(() => {
if (abortControllerRef.current) {
abortControllerRef.current.abort()
}
abortControllerRef.current = new AbortController()
// Make request with abort signal
fetch('/api/check', { signal: abortControllerRef.current.signal })
}, [value])
```
---
## 6. Server Error Mapping
**Error**: Server validation errors don't map to form fields
**Solution**:
```typescript
const onSubmit = async (data) => {
try {
const response = await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify(data),
})
if (!response.ok) {
const { errors } = await response.json()
// Map server errors to form fields
Object.entries(errors).forEach(([field, message]) => {
setError(field, {
type: 'server',
message: Array.isArray(message) ? message[0] : message,
})
})
return
}
} catch (error) {
setError('root', {
type: 'server',
message: 'Network error',
})
}
}
```
---
## 7. Default Values Not Applied
**Error**: Form fields don't show default values
**Cause**: Setting defaultValues after form initialization
**Solution**:
```typescript
// BAD - Set in useState
const [defaultValues, setDefaultValues] = useState({})
useEffect(() => {
setDefaultValues({ email: 'user@example.com' }) // Too late!
}, [])
const form = useForm({ defaultValues })
// GOOD - Set directly or use reset()
const form = useForm({
defaultValues: { email: 'user@example.com' },
})
// OR fetch and use reset
useEffect(() => {
async function loadData() {
const data = await fetchData()
reset(data)
}
loadData()
}, [reset])
```
---
## 8. Controller Field Not Updating
**Error**: Custom component doesn't update when value changes
**Cause**: Not spreading {...field} in Controller render
**Solution**:
```typescript
// BAD
<Controller
render={({ field }) => (
<CustomInput value={field.value} onChange={field.onChange} />
)}
/>
// GOOD - Spread all field props
<Controller
render={({ field }) => (
<CustomInput {...field} />
)}
/>
```
---
## 9. useFieldArray Key Warnings
**Error**: React warning about duplicate keys in list
**Symptoms**:
```
Warning: Encountered two children with the same key
```
**Solution**:
```typescript
// BAD - Using index as key
{fields.map((field, index) => (
<div key={index}>...</div>
))}
// GOOD - Use field.id
{fields.map((field) => (
<div key={field.id}>...</div>
))}
```
---
## 10. Schema Refinement Error Paths
**Error**: Custom validation errors appear at wrong field
**Cause**: Not specifying path in refinement
**Solution**:
```typescript
// BAD - Error appears at form level
z.object({
password: z.string(),
confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
// Missing path!
})
// GOOD - Error appears at confirmPassword field
z.object({
password: z.string(),
confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ['confirmPassword'], // Specify path
})
```
---
## 11. Transform vs Preprocess Confusion
**Error**: Data transformation doesn't work as expected
**When to use each**:
```typescript
// Use TRANSFORM for output transformation (after validation)
z.string().transform((val) => val.toUpperCase())
// Input: 'hello' -> Validation: passes -> Output: 'HELLO'
// Use PREPROCESS for input transformation (before validation)
z.preprocess(
(val) => (val === '' ? undefined : val),
z.string().optional()
)
// Input: '' -> Preprocess: undefined -> Validation: passes
```
---
## 12. Multiple Resolver Conflicts
**Error**: Form validation doesn't work with multiple resolvers
**Cause**: Trying to use multiple validation libraries simultaneously
**Solution**:
```typescript
// BAD - Can't use multiple resolvers
const form = useForm({
resolver: zodResolver(schema),
resolver: yupResolver(schema), // Overrides previous
})
// GOOD - Use single resolver, combine schemas if needed
const schema1 = z.object({ email: z.string() })
const schema2 = z.object({ password: z.string() })
const combinedSchema = schema1.merge(schema2)
const form = useForm({
resolver: zodResolver(combinedSchema),
})
```
---
## Debugging Tips
### Enable DevTools
```bash
npm install @hookform/devtools
```
```typescript
import { DevTool } from '@hookform/devtools'
<DevTool control={control} />
```
### Log Form State
```typescript
useEffect(() => {
console.log('Form State:', formState)
console.log('Errors:', errors)
console.log('Values:', getValues())
}, [formState, errors, getValues])
```
### Validate on Change During Development
```typescript
const form = useForm({
mode: 'onChange', // See errors immediately
resolver: zodResolver(schema),
})
```
---
**Official Docs**:
- React Hook Form: https://react-hook-form.com/
- Zod: https://zod.dev/

View File

@@ -0,0 +1,396 @@
# Comprehensive Zod Schemas Guide
Complete reference for all Zod schema types and patterns.
---
## Primitives
```typescript
// String
z.string()
z.string().min(3, "Min 3 characters")
z.string().max(100, "Max 100 characters")
z.string().length(10, "Exactly 10 characters")
z.string().email("Invalid email")
z.string().url("Invalid URL")
z.string().uuid("Invalid UUID")
z.string().regex(/pattern/, "Does not match pattern")
z.string().trim() // Trim whitespace
z.string().toLowerCase() //Convert to lowercase
z.string().toUpperCase() // Convert to uppercase
// Number
z.number()
z.number().int("Must be integer")
z.number().positive("Must be positive")
z.number().negative("Must be negative")
z.number().min(0, "Min is 0")
z.number().max(100, "Max is 100")
z.number().multipleOf(5, "Must be multiple of 5")
z.number().finite() // No Infinity or NaN
z.number().safe() // Within JS safe integer range
// Boolean
z.boolean()
// Date
z.date()
z.date().min(new Date("2020-01-01"), "Too old")
z.date().max(new Date(), "Cannot be in future")
// BigInt
z.bigint()
```
---
## Objects
```typescript
// Basic object
const userSchema = z.object({
name: z.string(),
age: z.number(),
})
// Nested object
const profileSchema = z.object({
user: userSchema,
address: z.object({
street: z.string(),
city: z.string(),
}),
})
// Partial (all fields optional)
const partialUserSchema = userSchema.partial()
// Deep Partial (recursively optional)
const deepPartialSchema = profileSchema.deepPartial()
// Pick specific fields
const nameOnlySchema = userSchema.pick({ name: true })
// Omit specific fields
const withoutAgeSchema = userSchema.omit({ age: true })
// Merge objects
const extendedUserSchema = userSchema.merge(z.object({
email: z.string().email(),
}))
// Passthrough (allow extra fields)
const passthroughSchema = userSchema.passthrough()
// Strict (no extra fields)
const strictSchema = userSchema.strict()
// Catchall (type for extra fields)
const catchallSchema = userSchema.catchall(z.string())
```
---
## Arrays
```typescript
// Array of strings
z.array(z.string())
// With length constraints
z.array(z.string()).min(1, "At least one item required")
z.array(z.string()).max(10, "Max 10 items")
z.array(z.string()).length(5, "Exactly 5 items")
z.array(z.string()).nonempty("Array cannot be empty")
// Array of objects
z.array(z.object({
name: z.string(),
age: z.number(),
}))
```
---
## Tuples
```typescript
// Fixed-length array with specific types
z.tuple([z.string(), z.number(), z.boolean()])
// With rest
z.tuple([z.string(), z.number()]).rest(z.boolean())
```
---
## Enums and Literals
```typescript
// Enum
z.enum(['red', 'green', 'blue'])
// Native enum
enum Color { Red, Green, Blue }
z.nativeEnum(Color)
// Literal
z.literal('hello')
z.literal(42)
z.literal(true)
```
---
## Unions and Discriminated Unions
```typescript
// Union
z.union([z.string(), z.number()])
// Discriminated union (recommended for better errors)
z.discriminatedUnion('type', [
z.object({ type: z.literal('user'), name: z.string() }),
z.object({ type: z.literal('admin'), permissions: z.array(z.string()) }),
])
```
---
## Optional and Nullable
```typescript
// Optional (value | undefined)
z.string().optional()
z.optional(z.string()) // Same as above
// Nullable (value | null)
z.string().nullable()
z.nullable(z.string()) // Same as above
// Nullish (value | null | undefined)
z.string().nullish()
```
---
## Default Values
```typescript
z.string().default('default value')
z.number().default(0)
z.boolean().default(false)
z.array(z.string()).default([])
```
---
## Refinements (Custom Validation)
```typescript
// Basic refinement
z.string().refine((val) => val.length > 5, {
message: "String must be longer than 5 characters",
})
// With custom path
z.object({
password: z.string(),
confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ['confirmPassword'],
})
// Multiple refinements
z.string()
.refine((val) => val.length >= 8, "Min 8 characters")
.refine((val) => /[A-Z]/.test(val), "Must contain uppercase")
.refine((val) => /[0-9]/.test(val), "Must contain number")
// Async refinement
z.string().refine(async (val) => {
const available = await checkAvailability(val)
return available
}, "Already taken")
```
---
## Transforms
```typescript
// String to number
z.string().transform((val) => parseInt(val, 10))
// Trim whitespace
z.string().transform((val) => val.trim())
// Parse date
z.string().transform((val) => new Date(val))
// Chain transform and refine
z.string()
.transform((val) => parseInt(val, 10))
.refine((val) => !isNaN(val), "Must be a number")
```
---
## Preprocess
```typescript
// Process before validation
z.preprocess(
(val) => (val === '' ? undefined : val),
z.string().optional()
)
// Convert to number
z.preprocess(
(val) => Number(val),
z.number()
)
```
---
## Intersections
```typescript
const baseUser = z.object({ name: z.string() })
const withEmail = z.object({ email: z.string().email() })
// Intersection (combines both)
const userWithEmail = baseUser.and(withEmail)
// OR
const userWithEmail = z.intersection(baseUser, withEmail)
```
---
## Records and Maps
```typescript
// Record (object with dynamic keys)
z.record(z.string()) // { [key: string]: string }
z.record(z.string(), z.number()) // { [key: string]: number }
// Map
z.map(z.string(), z.number())
```
---
## Sets
```typescript
z.set(z.string())
z.set(z.number()).min(1, "At least one item")
z.set(z.string()).max(10, "Max 10 items")
```
---
## Promises
```typescript
z.promise(z.string())
z.promise(z.object({ data: z.string() }))
```
---
## Custom Error Messages
```typescript
// Field-level
z.string({ required_error: "Name is required" })
z.number({ invalid_type_error: "Must be a number" })
// Validation-level
z.string().min(3, { message: "Min 3 characters" })
z.string().email({ message: "Invalid email format" })
// Custom error map
const customErrorMap: z.ZodErrorMap = (issue, ctx) => {
if (issue.code === z.ZodIssueCode.invalid_type) {
if (issue.expected === "string") {
return { message: "Please enter text" }
}
}
return { message: ctx.defaultError }
}
z.setErrorMap(customErrorMap)
```
---
## Type Inference
```typescript
const userSchema = z.object({
name: z.string(),
age: z.number(),
})
// Infer TypeScript type
type User = z.infer<typeof userSchema>
// Result: { name: string; age: number }
// Input type (before transforms)
type UserInput = z.input<typeof transformSchema>
// Output type (after transforms)
type UserOutput = z.output<typeof transformSchema>
```
---
## Parsing Methods
```typescript
// .parse() - throws on error
const result = schema.parse(data)
// .safeParse() - returns result object
const result = schema.safeParse(data)
if (result.success) {
console.log(result.data)
} else {
console.error(result.error)
}
// .parseAsync() - async validation
const result = await schema.parseAsync(data)
// .safeParseAsync() - async with result object
const result = await schema.safeParseAsync(data)
```
---
## Error Handling
```typescript
try {
schema.parse(data)
} catch (error) {
if (error instanceof z.ZodError) {
// Formatted errors
console.log(error.format())
// Flattened errors (for forms)
console.log(error.flatten())
// Individual issues
console.log(error.issues)
}
}
```
---
**Official Docs**: https://zod.dev