Forms Validation
Learn how to validate form input in React by tracking per-field errors in state, checking required fields and formats, and choosing when validation should run.
Introduction
Every form eventually needs to answer one question before it submits: is this data actually usable? A signup form with a blank name, a malformed email, or a password that is too short should not be sent to the server as-is. Validation is how you catch these problems and tell the user exactly what to fix, ideally before they ever hit "submit" and wait for a server error.
You already know how to build controlled inputs backed by state. This lesson builds directly on that: alongside the values themselves, you will track a parallel piece of state for errors, and use that state to both block invalid submissions and show helpful messages next to each field.
- How to check for required fields and simple format rules like email patterns.
- How to track a per-field error message in state and render it near the input.
- The tradeoffs between validating on submit, on blur, and on every keystroke.
- When to reach for a form library like React Hook Form or Formik instead of hand-rolled validation.
Why Validate on the Client?
Client-side validation is entirely about user experience, not security. A user can always bypass your React code — disable JavaScript, use dev tools, or call your API directly — so the server must always re-validate and never trust the client. What client-side validation gives you is instant feedback: the user finds out about a typo in their email address in milliseconds, instead of after a slow round trip to the backend.
Instant Feedback
Errors show up immediately, without waiting on a network request.
Guided Input
Per-field messages tell the user exactly what to fix and where.
Fewer Wasted Requests
Obviously invalid data never leaves the browser, saving a round trip.
Never a Substitute for Server Checks
The backend must always validate again — client checks are a convenience, not a security boundary.
Required Field Validation
The simplest validation rule is also the most common: does this field have a value at all? For text inputs, that usually means checking that the trimmed string is not empty.
function validateName(value) {
if (!value.trim()) {
return "Name is required.";
}
return "";
}This function returns an error message string when something is wrong, and an empty string when the field is valid. That convention makes it easy to store the result directly in an errors object and check whether any messages are non-empty before allowing submission.
Format Validation
Beyond "is it present," many fields need to match a particular shape. Email addresses are the classic example. A simple, "good enough for a teaching example" pattern checks for text, an @ symbol, more text, a dot, and a domain — it is not a fully RFC-compliant email validator (nothing simple truly is), but it catches the vast majority of typos.
function validateEmail(value) {
if (!value.trim()) {
return "Email is required.";
}
const simpleEmailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!simpleEmailPattern.test(value)) {
return "Please enter a valid email address.";
}
return "";
}Format validators usually run the required check first — an empty string should say "Email is required," not "Please enter a valid email address," since that is a more useful message for the user.
Tracking Errors in State
Just like form values live in state, so do form errors — typically as an object shaped like the form itself, where each key holds either an empty string (no error) or a message to display.
const [values, setValues] = useState({ name: "", email: "", password: "" });
const [errors, setErrors] = useState({ name: "", email: "", password: "" });Rendering an error is then just a conditional next to the matching input: if `errors.email` is non-empty, show it in a small message under the email field.
<div>
<label htmlFor="email">Email</label>
<input
id="email"
name="email"
value={values.email}
onChange={handleChange}
/>
{errors.email && <p className="field-error">{errors.email}</p>}
</div>Example: A Validated Signup Form
Putting this together, here is a small signup form that validates all fields on submit, blocks the submit handler when errors exist, and displays each message next to its input.
import { useState } from "react";
function validate(values) {
const errors = { name: "", email: "", password: "" };
if (!values.name.trim()) {
errors.name = "Name is required.";
}
const simpleEmailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!values.email.trim()) {
errors.email = "Email is required.";
} else if (!simpleEmailPattern.test(values.email)) {
errors.email = "Please enter a valid email address.";
}
if (values.password.length < 8) {
errors.password = "Password must be at least 8 characters.";
}
return errors;
}
function SignupForm() {
const [values, setValues] = useState({ name: "", email: "", password: "" });
const [errors, setErrors] = useState({ name: "", email: "", password: "" });
const [submitted, setSubmitted] = useState(false);
function handleChange(event) {
const { name, value } = event.target;
setValues((prev) => ({ ...prev, [name]: value }));
}
function handleSubmit(event) {
event.preventDefault();
const validationErrors = validate(values);
setErrors(validationErrors);
const hasErrors = Object.values(validationErrors).some(Boolean);
if (!hasErrors) {
setSubmitted(true);
console.log("Submitting:", values);
}
}
return (
<form onSubmit={handleSubmit} noValidate>
<div>
<label htmlFor="name">Name</label>
<input id="name" name="name" value={values.name} onChange={handleChange} />
{errors.name && <p className="field-error">{errors.name}</p>}
</div>
<div>
<label htmlFor="email">Email</label>
<input id="email" name="email" value={values.email} onChange={handleChange} />
{errors.email && <p className="field-error">{errors.email}</p>}
</div>
<div>
<label htmlFor="password">Password</label>
<input id="password" name="password" type="password" value={values.password} onChange={handleChange} />
{errors.password && <p className="field-error">{errors.password}</p>}
</div>
<button type="submit">Sign Up</button>
{submitted && <p>Account created!</p>}
</form>
);
}
export default SignupForm;Name is required.
Please enter a valid email address.
Password must be at least 8 characters.Validate on Submit vs Blur vs Change
When validation actually runs is a UX decision as much as a technical one. Each timing has a real tradeoff.
On Submit
Run every validator once, when the form is submitted.
- Simple to implement.
- No interruptions while typing.
- User only finds out about mistakes at the very end.
On Blur
Validate a field when the user leaves it (loses focus).
- Feedback arrives right after the user finishes a field.
- Does not flag errors mid-typing.
- A good middle ground for most forms.
On Change
Validate on every keystroke.
- Fastest possible feedback.
- Can feel harsh — flags "invalid" before the user has finished typing.
- Best reserved for things like password-strength meters.
A common, pragmatic pattern combines these: validate on blur to give per-field feedback as the user moves through the form, then re-validate everything on submit as a final safety net before the data is sent anywhere.
Form Libraries for Larger Apps
The hand-rolled approach above works well for small forms, but larger applications with many fields, nested data, or complex conditional rules often reach for a dedicated form library instead. Two of the most popular in the React ecosystem are React Hook Form and Formik.
React Hook Form
Minimizes re-renders by relying on refs and uncontrolled inputs internally, while still giving you validation, error messages, and schema integration.
Formik
One of the earliest popular React form libraries, offering a structured way to manage values, errors, and submission state together.
Schema Validation
Both libraries pair well with schema tools like Zod or Yup, letting you describe validation rules declaratively instead of writing manual if-checks.
You do not need these libraries to understand validation — everything in this lesson works with plain useState. But once forms grow past a handful of fields, a library removes a lot of repetitive boilerplate, and it is worth knowing they exist for future projects.
Common Mistakes
- Only validating on the client and trusting that data on the server — always re-validate server-side.
- Showing every error message on every field the moment the form loads, before the user has typed anything.
- Using an overly strict or exotic email regex that rejects valid addresses — a simple pattern is usually enough.
- Forgetting to clear a field's error once the user fixes it, leaving a stale message on screen.
- Blocking the submit button entirely instead of showing why submission failed — prefer letting submit run and displaying clear errors.
Best Practices
- Keep validator functions small, pure, and reusable across fields and forms.
- Store errors in state shaped like your values, so each field can look up its own message.
- Prefer validating on blur for most fields, then re-validate everything on submit as a final check.
- Always re-validate on the server — client-side validation is a UX feature, not a security control.
- Reach for React Hook Form or Formik once a form grows large enough that manual state management becomes repetitive.
Frequently Asked Questions
Should I validate on every keystroke?
Usually not for most fields — it can feel intrusive to see "invalid" while you are still typing. On-blur validation, combined with a final check on submit, is a friendlier default.
Is a simple regex enough for email validation?
For teaching purposes and most real forms, yes. A perfect RFC-compliant email regex is famously complex, and the real guarantee that an email works comes from actually sending a verification email, not from client-side pattern matching.
Do I need a form library to build a working form?
No. useState and a few validator functions are enough for small to medium forms, as shown in this lesson. Libraries become more valuable as forms grow larger or more dynamic.
Where should the "real" validation happen?
On the server. Client-side validation only improves the experience for well-behaved users; the server must always independently verify incoming data before trusting or storing it.
How do I disable the submit button until the form is valid?
You can derive a boolean like `const isValid = Object.values(errors).every((e) => !e)` and use it to control the button's disabled attribute, though many apps prefer to let users submit and see clear error messages instead.
Key Takeaways
- Required and format checks are simple functions that return an error message or an empty string.
- Errors live in state shaped like the form's values, so each field can render its own message.
- Validating on blur gives timely feedback without interrupting typing; validating on submit is a necessary final check.
- Client-side validation is a UX convenience — the server must always re-validate.
- Form libraries like React Hook Form and Formik remove boilerplate once forms grow large.
Summary
Form validation is really just state management applied to error messages: a validator function decides if a value is acceptable, and an errors object drives what the user sees. Choosing when to run that validation — on change, blur, or submit — is as important as the rules themselves.
In this lesson, you built required and format validators, tracked per-field errors in state, and weighed the tradeoffs of different validation timings. Next, you will use a similar form-handling foundation to build a login flow, and learn the basics of authenticating users in a React application.
- You can write required and format validators for form fields.
- You can track and display per-field errors using state.
- You understand the tradeoffs between validating on submit, blur, and change.
- You know when a form library like React Hook Form or Formik is worth adopting.