पाठ 14 / 25

Controlled Forms and Validation

State is the source of truth for inputs.

value plus onChange

A controlled input gets its value from state and updates that state in onChange, so React always knows the current value and can validate, transform (trim, uppercase) or disable buttons as the user types. Call event.preventDefault() in the form's onSubmit to handle submission in JavaScript. Show validation messages only after the user has typed something, and always validate again on the server: client validation is for convenience, not security.

User input and structured updates

Controlled inputs make forms predictable; reducers organise complex state changes.

Three ideas: controlled forms, useReducer, form actions.
Figure 5.1 — Controlled forms, reducers and actions.

An email form with live validation, run

I ran this with React 19.3 and react-dom 19.3 on Node.js 22, compiling JSX with esbuild 0.28. Browser-style examples use jsdom 29 as a fake DOM (imported from a small setup-dom.mjs file) and act() to apply updates. Typing "asha@" keeps the button disabled and shows "Enter a valid email"; typing a full address enables the button, and clicking it submits the trimmed value. The test sets the input value the way a browser does and fires an input event.

import "./setup-dom.mjs";   // test setup: jsdom provides a browser-like DOM in Node
import { useState, act } from "react";
import { createRoot } from "react-dom/client";

function Signup({ onSubmit }) {
  const [email, setEmail] = useState("");
  const valid = /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email);
  return (
    <form onSubmit={e => { e.preventDefault(); if (valid) onSubmit(email); }}>
      <input value={email} onChange={e => setEmail(e.target.value.trim())} />
      <button disabled={!valid}>Sign up</button>
      {!valid && email && <span>Enter a valid email</span>}
    </form>
  );
}
const submitted = [];
const root = createRoot(document.getElementById("root"));
await act(() => root.render(<Signup onSubmit={e => submitted.push(e)} />));
const input = document.querySelector("input");
function type(value) {   // set the value the way a browser does, then fire an input event
  const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
  setter.call(input, value);
  input.dispatchEvent(new window.Event("input", { bubbles: true }));
}
await act(() => type("asha@"));
console.log("button disabled:", document.querySelector("button").disabled, "|", document.querySelector("span")?.textContent);
await act(() => type("asha@example.com"));
console.log("button disabled:", document.querySelector("button").disabled);
await act(() => document.querySelector("button").click());
console.log("submitted:", submitted);

Output:

button disabled: true | Enter a valid email
button disabled: false
submitted: [ 'asha@example.com' ]

Use a form library for big forms

For many fields, nested data and complex validation, libraries such as React Hook Form with a schema validator (Zod) save a lot of code.

त्वरित जाँच: What makes an input "controlled"?

  • It uses a ref only
  • It has a CSS class
  • It is inside a form tag
  • Its value comes from React state and changes go through onChange
Answer

Its value comes from React state and changes go through onChange — State is the source of truth.