React Curriculum

Form Validation & Schema Integration (Zod)

Medium100% Free~25 mins#react#forms#validation#zod#schema-validation#touched-state#accessibility
Interview Answer (30-Second Summary)

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';
3
4// 1. Define schema & infer TypeScript interface
5export 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});
9
10type SignUpFormData = z.infer<typeof SignUpSchema>;
11
12export 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>>>({});
16
17 // Validate single field on blur
18 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 }
28
29 return (
30 <form>
31 <div>
32 <input
33 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 for touched fields 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
Why use schema validation libraries like Zod instead of manual if checks?

Schemas centralize validation rules in one place, automatically infer TypeScript types, eliminate runtime crashes via safeParse, and can be shared between frontend and backend.

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.

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.

It returns an object with { success: true, data } if valid, or { success: false, error } if invalid, avoiding try/catch blocks.

Use aria-invalid="true" on the input and aria-errormessage="error-id" referencing the ID of the error text element.