Form validation is one of the most common UI engineering problems. The 2026 ecosystem has consolidated around a small number of patterns: React Hook Form for React, native HTML validation for simple cases, Zod (or Yup) for schema definition. Frontend interviews increasingly test whether you know the modern stack and the tradeoffs.
Native HTML validation
The browser provides built-in validation:
requiredmin,maxfor numbers and datesminlength,maxlengthfor stringspatternfor regex validationtype="email",type="url"
The browser handles error messages and prevents submission. Customize messages with setCustomValidity().
Use case: simple forms with simple rules. No JS framework needed. Excellent accessibility by default.
React Hook Form (RHF)
The dominant React form library in 2026. Key benefits:
- Uncontrolled by default — no re-renders on every keystroke
- Tiny bundle (~10KB)
- Excellent TypeScript inference
- Built-in validation hooks
Basic usage:
const { register, handleSubmit, formState: { errors } } = useForm();
<input {...register("email", { required: true, pattern: /^.+@.+..+$/ })} />
<button onClick={handleSubmit(onSubmit)}>Submit</button>
Validation is field-level (on blur or change) and form-level (on submit).
Formik
The historical leader; still in use but losing share. More verbose API, controlled inputs, slower for large forms. New code typically picks RHF.
Schema validation with Zod
Zod (or Yup) defines validation rules as schemas:
const schema = z.object({
email: z.string().email(),
age: z.number().int().positive(),
password: z.string().min(8),
});
Combine with RHF:
const { register, handleSubmit } = useForm({
resolver: zodResolver(schema)
});
Benefits: schema is the single source of truth. Same schema can validate API responses, generate types, and validate forms.
Server-side validation
Always validate on the server. Frontend validation is UX; server validation is security. Never trust the client.
Best practice: share schemas between client and server. With Zod, the same schema runs in both. With Yup or Joi, similar approaches work.
Async validation
Common case: “is this username taken?” Hits the server, returns true/false.
RHF supports async validators that return promises. Debounce on input, cancel in-flight requests on new input. Show loading indicator.
Multi-step forms (wizards)
Patterns:
- Single Form provider, multiple steps within it (RHF supports nested register)
- Independent forms per step, with shared state via Zustand or context
- Validation per step on Next button
File uploads in forms
Tricky because file inputs are uncontrolled. RHF handles file refs natively. Validate:
- File size — reject before upload
- MIME type — for security
- Image dimensions — for image inputs
Accessibility
- Every input has a real
<label>(not placeholder text) - Errors are associated via
aria-describedby - Invalid fields have
aria-invalid="true" - Error messages appear in DOM order with the input, not after a long delay
- Focus management on submit failure: focus the first invalid field
The submit pattern
- Disable submit button to prevent double-submit
- Show loading state
- Validate (client) → submit (network) → server validates → response
- On success: redirect or show confirmation
- On failure: re-enable submit, focus problem field, show specific error
Common mistakes
- Live validation that fires on every keystroke (overwhelming)
- Showing all errors at submit instead of inline as user fixes them
- Disabling submit until form is valid (frustrating; user cannot test handler)
- Not validating on the server (security hole)
- Using placeholder as label (accessibility fail)
Frequently Asked Questions
Should I use React Hook Form or stick with controlled inputs?
RHF for non-trivial forms. Controlled inputs work for 1–2 fields but get expensive with re-renders.
Should validation run on every keystroke?
Generally no. Validate on blur (after the user has finished typing) and on submit. Live validation only for quick-fix cases (password strength, character count).
How do I handle multi-language error messages?
Validation libraries return error keys; map keys to translated strings via i18n library. Don’t hardcode English messages.