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
- The browser’s built-in validation: attributes such as
required,type="email",minLength,pattern,minandmax. It is free, accessible and works without JavaScript. See Form Validation. - 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.
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:
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:
| Strategy | Feels like | Good for |
|---|---|---|
| Only after the first submit | Calm | Short forms; used above |
After the user leaves a field (onBlur) | Helpful | Most forms |
| While typing, after the field was touched | Responsive | Passwords, usernames |
| While typing, from the first character | Nagging (“invalid email” after one letter) | Avoid |
To validate on blur, track which fields were touched:
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.