Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

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:

src/components/ReviewForm.jsx
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

HTMLReact
<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).