React Form.co
Blog/Validation

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.

ReactForm Team··10 min read

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.

jsx
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:

jsx
<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

jsx
if (!value.trim()) errs.fieldName = "This field is required";

Email format

jsx
if (!/S+@S+.S+/.test(value)) errs.email = "Enter a valid email address";

Minimum length

jsx
if (value.trim().length < 8) errs.password = "Must be at least 8 characters";

Maximum length

jsx
if (value.length > 500) errs.message = "Cannot exceed 500 characters";

Number range

jsx
const num = Number(value);
if (isNaN(num) || num < 1 || num > 100) {
  errs.quantity = "Enter a number between 1 and 100";
}

URL format

jsx
try {
  new URL(value);
} catch {
  errs.website = "Enter a valid URL including https://";
}

Phone number (basic)

jsx
if (!/^+?[ds-().]{7,15}$/.test(value)) {
  errs.phone = "Enter a valid phone number";
}

Matching fields (confirm password)

jsx
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.

jsx
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:

jsx
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:

jsx
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.

jsx
<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