| name | form-validation-architect |
| description | End-to-end form handling with react-hook-form, Zod schemas, validation patterns, error messaging, field arrays, and multi-step wizards. Use for complex forms, validation architecture, autosave, field dependencies. Activate on "form validation", "react-hook-form", "Zod", "form error", "multi-step form", "wizard". NOT for simple HTML forms, backend validation only, or non-React frameworks. |
| allowed-tools | Read,Write,Edit,Bash(npm:*) |
| metadata | {"category":"Code Quality & Testing","tags":["form","validation","architect","form-validation","react-hook-form"],"pairs-with":[{"skill":"typescript-advanced-patterns","reason":"Zod schemas and generic form types leverage advanced TypeScript patterns"},{"skill":"rest-api-design","reason":"Form validation schemas should match API endpoint request validation for consistency"},{"skill":"ux-friction-analyzer","reason":"Form UX friction (error messages, field ordering) is a primary usability concern"},{"skill":"react-performance-optimizer","reason":"Large forms with field arrays require React render optimization for responsiveness"}]} |
Form Validation Architect
Expert in building production-grade form systems with client-side validation, type safety, and excellent UX.
When to Use
✅ Use for:
- Complex forms with multiple fields and validation rules
- Multi-step wizards with progress tracking
- Dynamic field arrays (add/remove items)
- Form state persistence across sessions
- Async validation (check username availability, validate address)
- Dependent fields (enable B when A is checked)
- File uploads with progress and validation
- Autosave and optimistic updates
❌ NOT for:
- Simple contact forms (HTML + basic JS is fine)
- Backend-only validation (use Joi, Yup on server)
- Non-React frameworks (use Formik alternatives)
- Read-only displays (no form needed)
Quick Decision Tree
Does your form:
├── Have >5 fields? → Use react-hook-form
├── Need type safety? → Add Zod schemas
├── Have dynamic fields? → Use field arrays
├── Span multiple steps? → Use wizard pattern
├── Need async validation? → Use resolver + async rules
└── Just email/message? → Use native HTML validation
Technology Selection (2024+)
React Hook Form (Recommended)
Why RHF over Formik:
- Performance: Uncontrolled inputs → fewer re-renders
- Bundle size: 8KB vs 30KB (Formik)
- DevEx: Better TypeScript support
- Adoption: 40k+ stars, industry standard 2023+
Timeline:
- 2015-2019: Formik dominated
- 2019: React Hook Form released
- 2022+: RHF became standard
- 2024: Formik in maintenance mode
Zod for Schema Validation
Why Zod over Yup:
- TypeScript-first: Infer types from schemas
- Composability: Better schema reuse
- Error messages: More customizable
- Modern: Active development, latest features
Timeline:
- 2017-2020: Yup standard
- 2020: Zod released
- 2023+: Zod preferred for new projects
Common Anti-Patterns
Anti-Pattern 1: Controlled Inputs Everywhere
Novice thinking: "All form inputs should be controlled with useState"
Problem: Causes re-render on every keystroke
Wrong approach:
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [name, setName] = useState('');
<input value={email} onChange={(e) => setEmail(e.target.value)} />
Correct approach:
const { register, handleSubmit } = useForm();
<input {...register('email')} />
<input {...register('password')} />
<input {...register('name')} />
Why it matters: Forms with 10+ fields become sluggish with controlled inputs.
Anti-Pattern 2: String-Based Validation
Problem: No type safety, easy to make mistakes
Wrong approach:
const validate = (values) => {
if (!values.email.includes('@')) return 'Invalid email';
if (values.age < 18) return 'Must be 18+';
};
Correct approach:
const schema = z.object({
email: z.string().email('Invalid email'),
age: z.number().min(18, 'Must be 18+'),
username: z.string()
.min(3, 'Too short')
.regex(/^[a-z0-9_]+$/, 'Lowercase, numbers, underscores only')
});
type FormData = z.infer<typeof schema>;
Timeline:
- Pre-2020: String-based validation common
- 2020+: Schema-first validation standard
- 2024: Type inference from schemas expected
Anti-Pattern 3: No Error State Management
Problem: Errors shown before user interacts
Wrong approach:
{errors.email && <span>{errors.email}</span>}
Correct approach:
const { formState: { errors, touchedFields } } = useForm();
{touchedFields.email && errors.email && (
<span className="error">{errors.email.message}</span>
)}
const form = useForm({
mode: 'onBlur'
});
Why it matters: Better UX → user isn't yelled at before typing
Anti-Pattern 4: No Async Validation
Problem: Can't check username availability, validate addresses, etc.
Correct approach:
const schema = z.object({
username: z.string().refine(
async (username) => {
const available = await checkUsernameAvailability(username);
return available;
},
{ message: 'Username already taken' }
)
});
register('username', {
validate: {
checkAvailable: async (value) => {
const response = await fetch(`/api/check-username?q=${value}`);
return response.ok || 'Username taken';
}
}
});
Best practice: Debounce async validation to avoid API spam
Anti-Pattern 5: No Loading States
Problem: User doesn't know validation is happening
Correct approach:
const { formState: { isValidating, isSubmitting } } = useForm();
<button disabled={isValidating || isSubmitting}>
{isSubmitting ? 'Submitting...' :
isValidating ? 'Checking...' :
'Submit'}
</button>
Implementation Patterns
Pattern 1: Basic Form with Zod
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const loginSchema = z.object({
email: z.string().email('Invalid email address'),
password: z.string().min(8, 'Password must be at least 8 characters'),
rememberMe: z.boolean().optional()
});
type LoginForm = z.infer<typeof loginSchema>;
function LoginForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting }
} = useForm<LoginForm>({
resolver: zodResolver(loginSchema),
defaultValues: {
rememberMe: false
}
});
const onSubmit = async (data: LoginForm) => {
await api.login(data);
};
return (
< =>
{errors.email && {errors.email.message}}
{errors.password && {errors.password.message}}
Remember me
{isSubmitting ? 'Logging in...' : 'Login'}
);
}
Pattern 2: Multi-Step Wizard
const stepSchemas = [
z.object({
firstName: z.string().min(1, 'Required'),
lastName: z.string().min(1, 'Required'),
email: z.string().email()
}),
z.object({
street: z.string().min(1, 'Required'),
city: z.string().min(1, 'Required'),
zipCode: z.string().regex(/^\d{5}$/, 'Invalid ZIP')
}),
z.object({
cardNumber: z.string().regex(/^\d{16}$/, 'Invalid card'),
expiry: z.string().regex(/^\d{2}\/\d{2}$/, 'MM/YY format'),
cvv: z.string().regex(/^\d{3}$/, '3 digits')
})
];
function () {
[step, setStep] = ();
[formData, setFormData] = ({});
form = ({
: (stepSchemas[step])
});
= () => {
isValid = form.();
(isValid) {
({ ...formData, ...form.() });
(step + );
}
};
= () => {
({ ...formData, ...form.() });
(step - );
};
= () => {
finalData = { ...formData, ...data };
api.(finalData);
};
(
);
}
Pattern 3: Dynamic Field Arrays
const schema = z.object({
items: z.array(z.object({
name: z.string().min(1, 'Required'),
quantity: z.number().min(1, 'At least 1'),
price: z.number().min(0, 'Must be positive')
})).min(1, 'Add at least one item')
});
function OrderForm() {
const { register, control, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(schema),
defaultValues: {
items: [{ name: '', quantity: 1, price: 0 }]
}
});
const { fields, append, remove } = useFieldArray({
control,
name: 'items'
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
{fields.map((field, index) => (
<div key=>
remove(index)}>
Remove
))}
append({ name: '', quantity: 1, price: 0 })}>
Add Item
Submit Order
);
}
Pattern 4: Autosave (Debounced)
import { useDebounce } from 'use-debounce';
import { useEffect } from 'react';
function AutosaveForm() {
const { watch, register } = useForm();
const formValues = watch();
const [debouncedValues] = useDebounce(formValues, 1000);
useEffect(() => {
localStorage.setItem('draft', JSON.stringify(debouncedValues));
}, [debouncedValues]);
return (
<form>
<input {...register('title')} placeholder="Title" />
<textarea {...register('content')} placeholder="Content" />
<small>Autosaved</small>
</form>
);
}
Form UX Best Practices
1. Validate on Blur (Not on Change)
const form = useForm({
mode: 'onBlur'
});
2. Disable Submit While Invalid
<button
type="submit"
disabled={!form.formState.isValid || form.formState.isSubmitting}
>
Submit
</button>
3. Focus First Error on Submit
const onSubmit = async (data) => {
try {
await api.submit(data);
} catch (error) {
const firstError = Object.keys(errors)[0];
form.setFocus(firstError);
}
};
4. Optimistic UI Updates
const onSubmit = async (data) => {
setItems([...items, data]);
try {
await api.createItem(data);
} catch (error) {
setItems(items);
toast.error('Failed to save');
}
};
Production Checklist
□ Zod schemas for all forms
□ Type inference used (z.infer<typeof schema>)
□ Validation mode set appropriately (onBlur/onSubmit)
□ Error messages clear and actionable
□ Loading states for async operations
□ Focus management on errors
□ Autosave for long forms
□ Form state persisted (localStorage/session)
□ File upload progress indicators
□ Keyboard navigation tested
□ Accessibility (ARIA labels, error announcements)
□ Mobile-friendly (large touch targets)
When to Use vs Avoid
| Scenario | Use This Skill? |
|---|
| User registration with validation | ✅ Yes |
| Multi-step checkout flow | ✅ Yes |
| Dynamic form builder | ✅ Yes |
| Simple newsletter signup | ❌ No - use native HTML |
| Backend-only validation | ❌ No - use Joi/Yup on server |
| Non-React framework | ❌ No - use framework-specific solution |
Technology Comparison
| Feature | RHF + Zod | Formik + Yup | Native HTML5 |
|---|
| Performance | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| Type Safety | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ❌ |
| Bundle Size | 8KB | 30KB | 0KB |
| DevEx | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ |
| Field Arrays | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ❌ |
| Async Validation | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ❌ |
References
/references/zod-patterns.md - Advanced Zod schema patterns
/references/accessibility.md - Form accessibility guidelines
/references/file-upload.md - File upload with progress tracking
Scripts
scripts/generate_form.ts - Generate form from Zod schema
scripts/validate_schemas.ts - Lint Zod schemas for common issues
Assets
assets/form-templates/ - Ready-to-use form components
This skill guides: Form validation architecture | react-hook-form patterns | Zod schema design | Multi-step wizards | Field arrays | Autosave | Async validation