- React
- Form Architecture, Modern FormData, and Validation
- Form Validation & Schema Integration (Zod)
Form Validation & Schema Integration (Zod)
Schema-based validation decouples business validation rules from UI components. Using schemas (Zod or Yup) provides type inference, centralized error messages, and support for touched, dirty, and blur validation states without cluttering JSX.
Key Learning Objectives
Define validation schemas with Zod and infer TypeScript types automatically (z.infer<typeof Schema>).
Manage touched, dirty, and error states cleanly across multi-field forms.
Implement validation on blur vs validation on submit strategies.
Display field-level error messages accessibly using aria-invalid and aria-errormessage.
The Interview Problem & Context
Implement a type-safe form validation workflow using a Zod schema that validates input on blur and manages touched error states.
1import React, { useState } from 'react';2import { z } from 'zod';34// 1. Define schema & infer TypeScript interface5export const SignUpSchema = z.object({6 email: z.string().email('Invalid email address'),7 password: z.string().min(8, 'Password must be at least 8 characters'),8});910type SignUpFormData = z.infer<typeof SignUpSchema>;1112export function SignUpForm() {13 const [formData, setFormData] = useState<SignUpFormData>({ email: '', password: '' });14 const [errors, setErrors] = useState<Partial<Record<keyof SignUpFormData, string>>>({});15 const [touched, setTouched] = useState<Partial<Record<keyof SignUpFormData, boolean>>>({});1617 // Validate single field on blur18 function handleBlur(field: keyof SignUpFormData) {19 setTouched(prev => ({ ...prev, [field]: true }));20 const result = SignUpSchema.safeParse(formData);21 if (!result.success) {22 const fieldError = result.error.format()[field]?._errors[0];23 setErrors(prev => ({ ...prev, [field]: fieldError }));24 } else {25 setErrors(prev => ({ ...prev, [field]: undefined }));26 }27 }2829 return (30 <form>31 <div>32 <input33 name="email"34 value={formData.email}35 onBlur={() => handleBlur('email')}36 onChange={e => setFormData(p => ({ ...p, email: e.target.value }))}37 aria-invalid={Boolean(touched.email && errors.email)}38 />39 {touched.email && errors.email && <span className="error">{errors.email}</span>}40 </div>41 </form>42 );43}
Deep Architectural Explanation
Schema-Based Form Architecture
In naive forms, validation logic is scattered across inline if checks in event handlers. This leads to duplicate validation on the client and server and brittle error messages.
1. Decoupled Validation with Zod
With Zod:
- One schema acts as the single source of truth for client validation, server validation, and TypeScript types (
z.infer). schema.safeParse(data)never throws exceptions, returning a clean{ success, data, error }discriminated union.
2. Field States (Touched vs Dirty)
touched: User has focused and blurred out of the input. Rule: Only show error messages fortouchedfields so the user isn't yelled at before they even start typing!dirty: Current input value is different from initial default value. Useful for enabling or disabling the "Save Changes" button.
Common Interview Traps & Pitfalls
Showing red validation errors on initial mount before the user has interacted with the form.
Duplicating TypeScript interfaces manually instead of using z.infer<typeof Schema>.
Using schema.parse() (which throws exceptions) instead of schema.safeParse().
Senior Interviewer Follow-Up Probes
How do form libraries like React Hook Form integrate with Zod via zodResolver?
How do you handle asynchronous validation (e.g. checking username availability against a backend)?
How do aria-invalid and aria-errormessage improve screen reader accessibility for form errors?
Frequently Asked Interview Questions
What is the difference between "touched" and "dirty" form states?
Touched means the user has focused and blurred away from the field. Dirty means the field value has been modified from its original default value.
Why should validation errors only be shown after a field is touched?
Showing errors on untouched fields creates a hostile user experience by punishing users for incomplete data before they have had a chance to enter it.
What does safeParse return in Zod?
It returns an object with { success: true, data } if valid, or { success: false, error } if invalid, avoiding try/catch blocks.
How do you link error messages accessibly to inputs in HTML?
Use aria-invalid="true" on the input and aria-errormessage="error-id" referencing the ID of the error text element.
