Form Validation in React (No Library)
Form Validation in React
~12 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
Now let's build our first real form: a product review with name, star rating, and comment. In this chapter we validate "by hand", with plain React state – in the next chapter you'll learn about a library that automates much of this.
The principle: errors as their own state
Alongside the state for the form values themselves, we keep a SECOND state value for validation errors – usually an object whose keys match the field names:
const [values, setValues] = useState({ name: '', comment: '' });
const [errors, setErrors] = useState({});
function validate(values) {
const newErrors = {};
if (values.name.trim() === '') {
newErrors.name = 'Please enter your name.';
}
if (values.comment.trim().length < 10) {
newErrors.comment = 'The comment must be at least 10 characters long.';
}
return newErrors;
}Creating components/ReviewForm.jsx
Create src/components/ReviewForm.jsx – a complete, manually validated form with name, star rating (1–5), and comment:
import { useState } from 'react';
const INITIAL_VALUES = { name: '', rating: 5, comment: '' };
function validate(values) {
const errors = {};
if (values.name.trim() === '') {
errors.name = 'Please enter your name.';
}
if (values.comment.trim().length < 10) {
errors.comment = 'The comment must be at least 10 characters long.';
}
return errors;
}
function ReviewForm({ onSubmitReview }) {
const [values, setValues] = useState(INITIAL_VALUES);
const [errors, setErrors] = useState({});
function handleChange(event) {
const { name, value } = event.target;
setValues({ ...values, [name]: value });
}
function handleSubmit(event) {
event.preventDefault();
const validationErrors = validate(values);
setErrors(validationErrors);
if (Object.keys(validationErrors).length === 0) {
onSubmitReview(values);
setValues(INITIAL_VALUES);
}
}
return (
<form onSubmit={handleSubmit} className="review-form">
<div className="form-field">
<label htmlFor="name">Name</label>
<input
id="name"
name="name"
value={values.name}
onChange={handleChange}
/>
{errors.name && <p className="form-error">{errors.name}</p>}
</div>
<div className="form-field">
<label htmlFor="rating">Rating</label>
<select id="rating" name="rating" value={values.rating} onChange={handleChange}>
{[5, 4, 3, 2, 1].map((star) => (
<option key={star} value={star}>{star} {star === 1 ? 'star' : 'stars'}</option>
))}
</select>
</div>
<div className="form-field">
<label htmlFor="comment">Comment</label>
<textarea
id="comment"
name="comment"
value={values.comment}
onChange={handleChange}
/>
{errors.comment && <p className="form-error">{errors.comment}</p>}
</div>
<button type="submit">Submit review</button>
</form>
);
}
export default ReviewForm;A few details work together here: name="name"/name="rating" on each field allows ONE shared handleChange for all fields – event.target.name reveals which field changed, and {{ ...values, [name]: value }} (a "computed property name") updates precisely that one field in the object, without losing the others.
event.preventDefault() is crucial: without this line, the browser would, by default, reload the entire page when a <form> is submitted (the classic, pre-JavaScript way of processing forms) – that would wipe out our entire React state.
Two small but important HTML differences
| HTML | React |
|---|---|
<label for="name"> | <label htmlFor="name"> – for, like class, is a reserved JS word |
<option selected> | the selected option is controlled via value on the <select> itself, not on the <option> |
Tipp: This file isn't visible in the browser yet – we'll wire it into our real page in chapter 16 (real-world forms), once we also have our own product detail sub-page for it via routing (Phase 4).