React Form Validation - The Complete Guide Without Libraries
How to validate React forms properly without Yup, Zod, or React Hook Form. Covers required fields, formats, length limits, custom rules, and real-time feedback.
In this guide
Form validation is one of those topics where developers reach for a library out of habit before asking whether they actually need one. Yup, Zod, and React Hook Form are great tools, but they add weight, a learning curve, and an abstraction layer on top of something that is often not that complicated. This guide covers how to validate React forms properly without adding any dependencies.
1. Do you actually need a validation library?
The honest answer: it depends on the complexity of your form. A validation library makes sense when you have complex cross-field dependencies, deeply nested objects, async validation against an API, or reusable schema definitions shared between your frontend and backend.
It does not make sense for a contact form, a sign-up form, a settings form, or anything with fewer than 15 fields and straightforward rules. For those cases, a plain validation function is simpler, faster, and easier to debug.
2. The basic validation pattern
The pattern that works for most forms is a validate function that returns an errors object, called on submit. If the object has keys, show the errors and stop. If it is empty, proceed with the submission.
const [formData, setFormData] = useState({ name: "", email: "" });
const [errors, setErrors] = useState({});
const validate = (data) => {
const errs = {};
if (!data.name.trim()) errs.name = "Name is required";
if (!data.email.trim()) errs.email = "Email is required";
else if (!/S+@S+.S+/.test(data.email)) errs.email = "Enter a valid email";
return errs;
};
const handleSubmit = (e) => {
e.preventDefault();
const errs = validate(formData);
if (Object.keys(errs).length > 0) {
setErrors(errs);
return;
}
setErrors({});
// proceed with submission
};Display errors inline, directly beneath the field they belong to:
<div>
<label className="block text-sm font-medium mb-1">Email</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
className={errors.email ? "border-red-400" : "border-gray-200"}
/>
{errors.email && (
<p className="text-red-500 text-xs mt-1">{errors.email}</p>
)}
</div>3. Common validation rules
Here are the rules you will write repeatedly, all in one place:
Required field
if (!value.trim()) errs.fieldName = "This field is required";Email format
if (!/S+@S+.S+/.test(value)) errs.email = "Enter a valid email address";Minimum length
if (value.trim().length < 8) errs.password = "Must be at least 8 characters";Maximum length
if (value.length > 500) errs.message = "Cannot exceed 500 characters";Number range
const num = Number(value);
if (isNaN(num) || num < 1 || num > 100) {
errs.quantity = "Enter a number between 1 and 100";
}URL format
try {
new URL(value);
} catch {
errs.website = "Enter a valid URL including https://";
}Phone number (basic)
if (!/^+?[ds-().]{7,15}$/.test(value)) {
errs.phone = "Enter a valid phone number";
}Matching fields (confirm password)
if (formData.password !== formData.confirmPassword) {
errs.confirmPassword = "Passwords do not match";
}4. Real-time validation and touched state
Validating only on submit is fine for short forms. For longer forms, users benefit from seeing feedback as they complete each field. The standard approach is a touched object that tracks which fields have been visited.
const [touched, setTouched] = useState({});
const handleBlur = (e) => {
setTouched((prev) => ({ ...prev, [e.target.name]: true }));
};
// Only show error if the field has been touched
const getError = (name) => touched[name] ? errors[name] : null;Run validation on every change so the errors object is always up to date, but only display errors for touched fields:
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
const newData = {
...formData,
[name]: type === "checkbox" ? checked : value,
};
setFormData(newData);
// re-validate on every change so errors clear immediately when fixed
setErrors(validate(newData));
};5. Custom validation rules
Sometimes your validation logic depends on your own business rules. Since your validate function is just a plain JavaScript function, you can add anything:
const validate = (data) => {
const errs = {};
// Only allow company email addresses
if (data.email.endsWith("@gmail.com") || data.email.endsWith("@yahoo.com")) {
errs.email = "Please use your company email address";
}
// Require at least one checkbox to be selected
const hasSelection = data.options.some(Boolean);
if (!hasSelection) {
errs.options = "Select at least one option";
}
// Future date required
const selected = new Date(data.eventDate);
if (selected <= new Date()) {
errs.eventDate = "Event date must be in the future";
}
return errs;
};6. HTML5 native validation - when to use it
HTML5 provides built-in validation via attributes like required, minLength,maxLength, min, max, type=“email“, andpattern. The browser handles the rest.
<input
type="email"
name="email"
required
minLength={5}
maxLength={100}
pattern="[a-z0-9._%+-]+@[a-z0-9.-]+.[a-z]{2,}$"
/>Use native validation when you want basic protection with zero JavaScript. Avoid it when you need custom error messages, real-time feedback, or consistent cross-browser styling - the native browser validation UI looks different on every browser and cannot be styled.
For most production forms, native validation is a good safety net on top of your own JavaScript validation, not a replacement for it.
7. Skip writing validation entirely
If you are building a form to collect responses and do not need it embedded in your own app, you do not need to write any validation code at all. ReactForm.co lets you set required fields, min/max lengths, number ranges, and date constraints through the field editor - no code required.
The published form handles validation automatically before submission. You just configure the rules visually and share the link.
Build a validated form without writing validation
Set required fields, min/max rules, and date constraints visually. Publish and collect responses - free plan, no credit card.
Open the Form Builder