Skip to content
Laviyx Learn

Try

React course menu

React Forms: Controlled Inputs, Validation and Submit

Build forms in React: controlled inputs with value and onChange, text areas, selects, checkboxes and radio buttons, one handler for many fields, validation and error messages, submitting with preventDefault, and uncontrolled inputs with FormData.

Beginner10 min readUpdated
On this page

Forms collect input: sign-ups, searches, checkouts, settings. The standard React approach is the controlled input: React state holds the input's value, and every keystroke updates that state.

A controlled input#

JSX
import { useState } from "react";

export default function App() {
  const [city, setCity] = useState("");

  return (
    <div>
      <input value={city} onChange={(e) => setCity(e.target.value)} placeholder="Enter a city" />
      <p>{city ? `Searching flights to ${city}...` : "Type a city"}</p>
      <p>{city.length} / 30 characters</p>
    </div>
  );
}
Result
  • value={city} makes the input always show the state.
  • onChange runs on every keystroke with the new text in e.target.value; setting state updates the input.

Because the value lives in state, you can use it anywhere: live previews, character counts, enabling or disabling buttons, validation as you type.

Other input types#

JSX
import { useState } from "react";

export default function App() {
  const [message, setMessage] = useState("");
  const [plan, setPlan] = useState("basic");
  const [agree, setAgree] = useState(false);
  const [delivery, setDelivery] = useState("standard");

  return (
    <div style={{ display: "grid", gap: 10, maxWidth: 360 }}>
      <textarea value={message} onChange={(e) => setMessage(e.target.value)} rows={2} placeholder="Message" />

      <select value={plan} onChange={(e) => setPlan(e.target.value)}>
        <option value="basic">Basic — ₹199</option>
        <option value="pro">Pro — ₹499</option>
      </select>

      <div>
        {["standard", "express"].map((option) => (
          <label key={option} style={{ marginRight: 12 }}>
            <input
              type="radio"
              name="delivery"
              value={option}
              checked={delivery === option}
              onChange={(e) => setDelivery(e.target.value)}
            />{" "}
            {option}
          </label>
        ))}
      </div>

      <label>
        <input type="checkbox" checked={agree} onChange={(e) => setAgree(e.target.checked)} /> I agree to the terms
      </label>

      <p style={{ margin: 0 }}>
        Plan: {plan}, delivery: {delivery}, agreed: {agree ? "yes" : "no"}, message length: {message.length}
      </p>
    </div>
  );
}
Result
  • <textarea> and <select> use value in React (not inner text or selected).
  • Checkboxes use checked and e.target.checked.
  • Radio buttons share a name; each is checked when its value equals the state.

One state object and one handler#

For forms with many fields, keep them in one object and use the input's name to update the right field:

JSX
import { useState } from "react";

export default function App() {
  const [form, setForm] = useState({ fullName: "", email: "", age: "" });
  const [submitted, setSubmitted] = useState(null);
  const [errors, setErrors] = useState({});

  function handleChange(e) {
    const { name, value } = e.target;
    setForm({ ...form, [name]: value });
  }

  function validate(values) {
    const found = {};
    if (!values.fullName.trim()) found.fullName = "Name is required";
    if (!/^\S+@\S+\.\S+$/.test(values.email)) found.email = "Enter a valid email";
    if (values.age && (Number(values.age) < 13 || Number(values.age) > 120)) found.age = "Age must be 13–120";
    return found;
  }

  function handleSubmit(e) {
    e.preventDefault();
    const found = validate(form);
    setErrors(found);
    setSubmitted(Object.keys(found).length === 0 ? form : null);
  }

  const field = (name, label, type = "text") => (
    <label style={{ display: "block", marginBottom: 8 }}>
      {label}
      <br />
      <input name={name} type={type} value={form[name]} onChange={handleChange} />
      {errors[name] && <span style={{ color: "crimson", marginLeft: 8 }}>{errors[name]}</span>}
    </label>
  );

  return (
    <form onSubmit={handleSubmit} noValidate>
      {field("fullName", "Full name")}
      {field("email", "Email", "email")}
      {field("age", "Age (optional)", "number")}
      <button type="submit">Sign up</button>
      {submitted && <p style={{ color: "green" }}>Welcome, {submitted.fullName}! We sent a link to {submitted.email}.</p>}
    </form>
  );
}
Result

[name]: value is a computed property name: the key comes from the variable name. Try submitting with empty or invalid fields to see the error messages.

noValidate turns off the browser's own validation popups so React's messages are shown instead. In real apps you may keep both: browser attributes such as required and type="email" are a useful first layer.

Uncontrolled inputs with FormData#

For simple forms where you only need the values on submit, you can let the browser keep the values and read them with FormData:

JSX
import { useState } from "react";

export default function App() {
  const [result, setResult] = useState(null);

  function handleSubmit(e) {
    e.preventDefault();
    setResult(Object.fromEntries(new FormData(e.currentTarget)));
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="product" defaultValue="Laptop stand" />
      <input name="qty" type="number" defaultValue={1} min={1} style={{ width: 60, marginLeft: 8 }} />
      <button style={{ marginLeft: 8 }}>Add to cart</button>
      {result && <pre>{JSON.stringify(result)}</pre>}
    </form>
  );
}
Result

defaultValue sets the starting value without controlling the input. Use controlled inputs when the UI must react while typing (validation, previews, dependent fields); uncontrolled ones are fine for simple submit-only forms. Libraries such as React Hook Form build on these ideas for large forms.

Common mistakes#

Practice#

Practice exercise

Build a password field with a "Show" checkbox that toggles between hidden and visible text, and a live strength message: "Too short" under 8 characters, "OK" otherwise, and "Strong" if it is at least 12 characters and contains a digit.

Show hint

Use two state values: password and show. Set type={show ? "text" : "password"}, and test for a digit with /\d/.test(password).

Show solution
JSX
import { useState } from "react";

export default function App() {
  const [password, setPassword] = useState("");
  const [show, setShow] = useState(false);

  let strength = "Too short";
  if (password.length >= 12 && /\d/.test(password)) strength = "Strong";
  else if (password.length >= 8) strength = "OK";

  return (
    <div>
      <input type={show ? "text" : "password"} value={password} onChange={(e) => setPassword(e.target.value)} />
      <label style={{ marginLeft: 8 }}>
        <input type="checkbox" checked={show} onChange={(e) => setShow(e.target.checked)} /> Show
      </label>
      <p>{password ? strength : "Enter a password"}</p>
    </div>
  );
}
Result

Key takeaway#

Next: Sharing State Between Components.

Finished this lesson?