import { useState } from "react";
function SignupForm() {
const [email, setEmail] = useState("");
const [error, setError] = useState("");
function handleSubmit(event) {
event.preventDefault();
if (!email.includes("@")) {
setError("Please enter a valid email address.");
return;
}
setError("");
console.log("Submitting:", email);
}
return (
<form onSubmit={handleSubmit}>
<input value={email} onChange={(e) => setEmail(e.target.value)} />
{error && <p style={{ color: "red" }}>{error}</p>}
<button type="submit">Sign Up</button>
</form>
);
}function validatePassword(password, confirmPassword) {
if (password.length < 8) {
return "Password must be at least 8 characters.";
}
if (password !== confirmPassword) {
return "Passwords do not match.";
}
return ""; // no error
}Writing validation as a small, standalone function like this (rather than inline inside handleSubmit) makes it reusable and much easier to unit test separately from your component.
const [errors, setErrors] = useState({});
function validate(form) {
const newErrors = {};
if (!form.name) newErrors.name = "Name is required";
if (!form.email.includes("@")) newErrors.email = "Invalid email";
return newErrors;
}
function handleSubmit(e) {
e.preventDefault();
const newErrors = validate(form);
setErrors(newErrors);
if (Object.keys(newErrors).length === 0) {
console.log("Form is valid!");
}
}Storing errors as an object keyed by field name lets you display each error message right next to its corresponding input, which is far more helpful to users than one generic error message at the top.
If a user fixes a mistake but the error message never disappears until they submit again, the form feels broken. Re-run validation (or at least clear that specific field's error) inside the onChange handler too, so feedback updates as the user types, not just on submit.
Build a signup form with name, email, and password fields, validating that all are filled in and the email contains an @ symbol before allowing submission.
import { useState } from "react";
function SignupForm() {
const [form, setForm] = useState({ name: "", email: "", password: "" });
const [error, setError] = useState("");
function handleChange(e) {
setForm(prev => ({ ...prev, [e.target.name]: e.target.value }));
}
function handleSubmit(e) {
e.preventDefault();
if (!form.name || !form.email || !form.password) {
setError("All fields are required.");
return;
}
if (!form.email.includes("@")) {
setError("Enter a valid email.");
return;
}
setError("");
console.log("Valid form:", form);
}
return (
<form onSubmit={handleSubmit}>
<input name="name" value={form.name} onChange={handleChange} placeholder="Name" />
<input name="email" value={form.email} onChange={handleChange} placeholder="Email" />
<input name="password" type="password" value={form.password} onChange={handleChange} placeholder="Password" />
{error && <p>{error}</p>}
<button type="submit">Sign Up</button>
</form>
);
}
Q Should validation happen on every keystroke or only on submit?
A common, user-friendly middle ground: validate on submit first, then re-validate that specific field on every change afterward, once the user has already seen an error for it โ this avoids being annoying to first-time typers.
Q Are there libraries that handle form validation for me?
Yes โ popular options like React Hook Form and Formik handle validation, error state, and submission boilerplate for you. They're worth learning once you're comfortable building forms manually, as shown in this chapter.