Handling Form Validation Without Losing Your Sanity
Comments
Sign in to join the conversation
Sign in to join the conversation
If you have ever built a complex form in React, you know the struggle. What starts as a simple login screen quickly spirals into a nightmare of useState, 50-line validation functions, and confusing useEffect hooks.
Managing form state manually is one of the quickest ways to bloat your component and hurt performance. The good news? You don't have to do it that way.
Here is how to handle form validation efficiently, cleanly, and without tearing your hair out.
The traditional React approach often looks like this:
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [errors, setErrors] = useState({});
const handleSubmit = (e) => {
e.preventDefault();
if (!name) setErrors(prev => ({ ...prev, name: 'Required' }));
// ... more manual logic
};
The industry standard for modern React forms is React Hook Form.
Unlike the traditional approach, React Hook Form uses uncontrolled components. It registers your inputs using ref, meaning the form state is managed outside of the React render cycle. This results in significantly better performance and less code.
registerInstead of writing onChange and value for every input, you just "register" it.
<input {...register("firstName")} />
Writing if (email.includes('@')) is prone to bugs. Instead, use a schema validation library like Zod.
Zod allows you to define the shape of your data separately from your UI. It reads like plain English.
import { z } from "zod";
const signUpSchema = z.object({
username: z.string().min(3, "Username must be at least 3 chars"),
email: z.string().email("Invalid email address"),
age: z.number().min(18, "You must be 18 or older"),
});
By combining React Hook Form with Zod (via @hookform/resolvers), you get the best of both worlds: performant inputs and robust validation.
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
// 1. Define your schema
const schema = z.object({
email: z.string().email("Please enter a valid email"),
password: z.string().min(8, "Password must be 8+ characters"),
})
Even with great tools, user experience matters.
onBlur (when they leave the field) or onChange (only after the first error appears).
useForm({ mode: 'onBlur', resolver: zodResolver(schema) })
isSubmitting state provided by the hook to disable the button while the API call is in progress.aria-describedby so screen readers can announce them.