Skip to content

React Tutorial Forms and Validation

Basic Client-Side Form Validation in React

Lesson 76 of 156 2 min read By Deepak Paswan Updated Oct 10, 2026

Runnable lesson · press Try it on the examples to edit and run them here.

Validation checks that the user’s input is acceptable before you use it. In the browser it gives quick feedback; it never replaces checking on the server.

Two layers

  1. The browser’s built-in validation: attributes such as required, type="email", minLength, pattern, min and max. It is free, accessible and works without JavaScript. See Form Validation.
  2. Your own rules in React: messages in your style, rules that depend on several fields, and control over when errors appear.

A validate function

Write validation as a pure function: values in, errors out.

App.jsx
JSX
function validate(values) {
  const errors = {};
  if (!values.name.trim()) errors.name = "Enter your name.";
  if (!/^\S+@\S+\.\S+$/.test(values.email)) errors.email = "Enter a valid email address.";
  if (values.password.length < 8) errors.password = "Use at least 8 characters.";
  if (values.confirm !== values.password) errors.confirm = "The passwords do not match.";
  return errors;
}

A function like this is easy to test, easy to reuse on the server, and does not depend on React.

Errors are derived

Do not store errors in state. Calculate them from the values on every render:

App.jsx
JSX
function validate(values) {
  const errors = {};
  if (!values.name.trim()) errors.name = "Enter your name.";
  if (!/^\S+@\S+\.\S+$/.test(values.email)) errors.email = "Enter a valid email address.";
  if (values.password.length < 8) errors.password = "Use at least 8 characters.";
  if (values.confirm !== values.password) errors.confirm = "The passwords do not match.";
  return errors;
}

function App() {
  const [values, setValues] = React.useState({ name: "", email: "", password: "", confirm: "" });
  const [submitted, setSubmitted] = React.useState(false);
  const [done, setDone] = React.useState("");

  const errors = validate(values);               // derived on every render
  const hasErrors = Object.keys(errors).length > 0;

  function handleChange(e) {
    setValues({ ...values, [e.target.name]: e.target.value });
  }

  function handleSubmit(e) {
    e.preventDefault();
    setSubmitted(true);                          // from now on, show errors
    if (!hasErrors) setDone("Welcome, " + values.name);
  }

  const show = (field) => submitted && errors[field];

  return (
    <form onSubmit={handleSubmit} noValidate style={{ display: "grid", gap: 8, maxWidth: 300 }}>
      <label>Name <input name="name" aria-label="Name" value={values.name} onChange={handleChange} /></label>
      {show("name") && <small style={{ color: "#c0392b" }}>{errors.name}</small>}

      <label>Email <input name="email" aria-label="Email" value={values.email} onChange={handleChange} /></label>
      {show("email") && <small style={{ color: "#c0392b" }}>{errors.email}</small>}

      <label>Password <input name="password" type="password" aria-label="Password" value={values.password} onChange={handleChange} /></label>
      {show("password") && <small style={{ color: "#c0392b" }}>{errors.password}</small>}

      <label>Confirm <input name="confirm" type="password" aria-label="Confirm" value={values.confirm} onChange={handleChange} /></label>
      {show("confirm") && <small style={{ color: "#c0392b" }}>{errors.confirm}</small>}

      <button type="submit">Sign up</button>
      <p role="status">{done}</p>
    </form>
  );
}

noValidate turns off the browser’s own popups so your messages take over. If you prefer the native messages, leave it out and use the HTML attributes.

When to show errors

This choice decides whether the form feels helpful or nagging:

StrategyFeels likeGood for
Only after the first submitCalmShort forms; used above
After the user leaves a field (onBlur)HelpfulMost forms
While typing, after the field was touchedResponsivePasswords, usernames
While typing, from the first characterNagging (“invalid email” after one letter)Avoid

To validate on blur, track which fields were touched:

App.jsx
JSX
const [touched, setTouched] = useState({});
<input name="email" onBlur={() => setTouched({ ...touched, email: true })} />
{touched.email && errors.email && <small>{errors.email}</small>}

Disabled submit or messages?

Disabling the submit button until everything is valid gives no explanation. A better pattern keeps the button enabled; on submit, show all errors and move focus to the first one (Validation Messages).

Do not over-validate

  • Email: a simple “has an @ and a dot” check is plenty. A strict regex rejects real addresses. The only true test is sending a confirmation message.
  • Names: do not assume a first name and a last name, or reject apostrophes, hyphens and accents.
  • Phone numbers: accept spaces and +; normalise later.

Always validate on the server

Anyone can bypass client-side checks (developer tools, scripts, API calls). The server must check everything again, and must never trust the browser. Client validation is for the user’s convenience, server validation is for safety.

Key points

  • Write validation as a pure function; derive errors from values instead of storing them.
  • Decide when to show errors: on submit, on blur or after touching.
  • Client validation helps users; the server must validate too.