React Form Validation Without a Library

Validate React forms with plain state and plain functions. Show accessible error messages and keep quick client checks separate from server validation.

6 min read

React form validation without a library means checking user input with plain state and plain functions before you accept a submission. The goal is a visible, accessible error for every invalid field, plus a clear boundary between quick client checks and authoritative server validation. You build it with controlled inputs, an errors object, and one validate function.

Validate on submit with a plain function

The validate function takes the current values and returns an object whose keys match the field names. A missing key means the field is valid, and a key with a message means the field failed.

index.jsindex.js
function validate(values) {
  const errors = {};
 
  if (!values.name.trim()) {
    errors.name = "Enter your name.";
  }
  if (!values.email.includes("@")) {
    errors.email = "Enter a valid email address.";
  }
 
  return errors;
}

Empty strings are falsy, so the name check catches a blank field, and the email check catches a missing @ sign. Returning an empty object tells the caller that every field passed.

Keep values and errors in state

The form holds two separate pieces of state: the values object and the errors object. The submit handler runs validate and stores the result, while the change handler updates one field at a time.

App.jsxApp.jsx
import { useState } from "react";
 
export default function SignupForm() {
  const [values, setValues] = useState({ name: "", email: "" });
  const [errors, setErrors] = useState({});
  function handleSubmit(e) {
    e.preventDefault();
    setErrors(validate(values));
  }
  function handleChange(e) {
    setValues({ ...values, [e.target.name]: e.target.value });
  }
  // the return below continues this same component

The change handler copies the previous object and replaces only the edited key, so the update stays immutable. The submit handler stops the page reload first, then fills the errors state with whatever validate found.

Show errors next to each field

Render each message beside its input and connect the two with ARIA so assistive technology announces the failure. This return block still belongs inside the SignupForm component above.

App.jsxApp.jsx
  return (
    <form onSubmit={handleSubmit} noValidate>
      <label htmlFor="name">Name</label>
      <input id="name" name="name" value={values.name} aria-invalid={errors.name ? "true" : "false"} aria-describedby="name-error" onChange={handleChange} />
      {errors.name && <p id="name-error" role="alert">{errors.name}</p>}
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" value={values.email} aria-invalid={errors.email ? "true" : "false"} aria-describedby="email-error" onChange={handleChange} />
      {errors.email && <p id="email-error" role="alert">{errors.email}</p>}
      <button type="submit">Sign up</button>
    </form>
  );
}

The noValidate prop turns off browser tooltips so your own messages stay consistent. aria-invalid flags the failed field, and aria-describedby points the message at it, so a screen reader announces the error when the field regains focus.

Validate on blur for failed fields

Validating only on submit is correct, but users also appreciate a recheck once they have already failed a field. Validate again when a field loses focus, and only if it already has an error.

App.jsxApp.jsx
function handleBlur(e) {
  if (errors[e.target.name]) {
    setErrors(validate(values));
  }
}

Attach handleBlur to each input. The guard means a clean field is not interrupted while the user is still typing, but a field that already failed is rechecked the moment the user leaves it, so the message clears as soon as it is fixed.

Check format, not just presence

A blank field is the easiest mistake to catch, but presence is rarely enough on its own. An email needs an @ sign, a password needs a minimum length, and a number must fall inside a range. Keep these checks in the same validate function and return one clear message per field.

When the rules grow large or need to be shared with the server, move them into a schema instead of hand-writing dozens of branches. The error object stays the same either way, so the UI does not care how a rule was checked. Start with presence, then layer in the format rules your data actually needs.

Separate client checks from server validation

The checks above are convenience, not security. Anyone can bypass client-side JavaScript, so the server must validate the same data again and decide whether the request is accepted.

  • Client validation gives fast, friendly feedback before the request is sent.
  • Server validation is authoritative and returns the real errors after the request.
  • Show server errors in the same errors object so the UI stays consistent.

The same errors state can hold messages the server returns, so one component renders both kinds of feedback. For the schema approach that replaces the hand-written validate function, see how to validate React forms with Zod.

For displaying the errors a server sends back, see how to show server validation errors in React forms. The basic form wiring is covered in how to build forms in React.

Rune AI

Rune AI

Key Insights

  • Keep values and errors in separate state.
  • Return an errors object from one validate function.
  • Show errors with role=alert and aria-describedby.
  • Validate on submit, then recheck failed fields on blur.
  • Client validation never replaces server validation.
RunePowered by Rune AI

Frequently Asked Questions

Do I need a validation library for React forms?

No. A plain function that returns an errors object is enough for most small forms. Reach for a schema library like Zod or a form library when the form grows large or needs many rules.

Should I validate on submit or on every keystroke?

Always validate on submit. Add per-field checks on blur or change only after a field has already failed, so users do not see errors while they are still typing.

Is client-side validation enough?

No. Client validation is a convenience for fast feedback. The server must revalidate the same data because any client check can be bypassed.

Conclusion

Validate React forms with a plain function and an errors object. Show each message accessibly, and treat client checks as convenience while the server stays the authority.