Initial commit
This commit is contained in:
359
references/accessibility.md
Normal file
359
references/accessibility.md
Normal 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
|
||||
255
references/error-handling.md
Normal file
255
references/error-handling.md
Normal 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/
|
||||
197
references/links-to-official-docs.md
Normal file
197
references/links-to-official-docs.md
Normal 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
|
||||
355
references/performance-optimization.md
Normal file
355
references/performance-optimization.md
Normal 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
|
||||
420
references/rhf-api-reference.md
Normal file
420
references/rhf-api-reference.md
Normal 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/
|
||||
390
references/shadcn-integration.md
Normal file
390
references/shadcn-integration.md
Normal 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
381
references/top-errors.md
Normal 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/
|
||||
396
references/zod-schemas-guide.md
Normal file
396
references/zod-schemas-guide.md
Normal 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
|
||||
Reference in New Issue
Block a user