flowchart LR
A["User Input"] --> B["React Hook Form"]
B --> C{"Zod Schema<br/>Validation"}
C -->|Valid| D["Submit Handler"]
C -->|Invalid| E["Error State"]
E --> F["Display Errors"]
D --> G["API Call"]
G -->|Success| H["Success Toast"]
G -->|Failure| I["Error Toast"]
style B fill:#ec4899
style C fill:#8b5cf6
style D fill:#10b981
Form Handling & Validation: Taming User Input
Forms are the backbone of interactive applications. In React, handling forms well means balancing developer experience, user experience, and data integrity.
This chapter covers modern form patterns using React Hook Form and Zod for type-safe validation.
The Problem with Native Forms
React’s controlled inputs require you to manage state for every field:
// 😰 This gets painful fast with 10+ fields
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
// ... and so onEvery keystroke triggers a re-render. Validation logic gets scattered. Error handling becomes a mess.
React Hook Form: The Modern Solution
React Hook Form uses uncontrolled inputs with refs, minimizing re-renders while giving you full control.
npm install react-hook-form zod @hookform/resolversBasic Example
import { useForm } from 'react-hook-form';
type FormData = {
email: string;
password: string;
};
function LoginForm() {
const { register, handleSubmit, formState: { errors } } = useForm<FormData>();
const onSubmit = (data: FormData) => {
console.log(data); // { email: '...', password: '...' }
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email', { required: 'Email is required' })} />
{errors.email && <span>{errors.email.message}</span>}
<input
type="password"
{...register('password', { minLength: { value: 8, message: 'Min 8 chars' } })}
/>
{errors.password && <span>{errors.password.message}</span>}
<button type="submit">Login</button>
</form>
);
}
Zod: Schema-First Validation
Instead of inline validation rules, define a schema that describes your data. This schema can be reused for API validation, type generation, and form validation.
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
// 1. Define the schema
const loginSchema = z.object({
email: z.string().email('Invalid email format'),
password: z.string().min(8, 'Password must be at least 8 characters'),
});
// 2. Infer the type from the schema
type LoginFormData = z.infer<typeof loginSchema>;
// 3. Use it in your form
function LoginForm() {
const { register, handleSubmit, formState: { errors } } = useForm<LoginFormData>({
resolver: zodResolver(loginSchema),
});
// ... same as before
}
Handling Complex Forms
Nested Objects
const profileSchema = z.object({
user: z.object({
firstName: z.string().min(1),
lastName: z.string().min(1),
}),
address: z.object({
street: z.string(),
city: z.string(),
zip: z.string().regex(/^\d{5}$/, 'Invalid ZIP'),
}),
});
// In your form:
<input {...register('user.firstName')} />
<input {...register('address.zip')} />
Dynamic Fields (Arrays)
import { useFieldArray } from 'react-hook-form';
function TeamForm() {
const { control, register } = useForm({
defaultValues: { members: [{ name: '' }] },
});
const { fields, append, remove } = useFieldArray({ control, name: 'members' });
return (
<>
{fields.map((field, index) => (
<div key={field.id}>
<input {...register(`members.${index}.name`)} />
<button type="button" onClick={() => remove(index)}>Remove</button>
</div>
))}
<button type="button" onClick={() => append({ name: '' })}>Add Member</button>
</>
);
}
Error Display Patterns
Inline Errors
<input {...register('email')} className={errors.email ? 'input-error' : ''} />
{errors.email && <p className="error-text">{errors.email.message}</p>}
Toast on Submit Failure
const onSubmit = async (data) => {
try {
await api.login(data);
} catch (err) {
toast.error('Login failed. Please check your credentials.');
}
};
Key Takeaways
| Pattern | When to Use |
|---|---|
| Controlled Inputs | Simple forms, <5 fields |
| React Hook Form | Any form with validation |
| Zod Schemas | Type-safe, reusable validation |
| useFieldArray | Dynamic lists of inputs |
Forms are the most common source of bugs in web apps. Investing in a solid form architecture pays dividends in maintainability.