CodeMIRAI
0 XP

Forms and controlled inputs

3 min read 4 runnable examples Quiz included

Controlled inputs

React (JSX)
import { useState } from "react";

function App() {
  const [name, setName] = useState("");
  return (
    <div>
      <input value={name} onChange={(e) => setName(e.target.value)} placeholder="Name" />{" "}
      <button onClick={() => setName("")}>Clear</button>
      <p>Hello, {name || "..."}</p>
    </div>
  );
}

A controlled input takes its value from state (value={name}), and every keystroke updates that state (onChange). State is the single source of truth, which is why the Clear button can empty the box just by changing state. Set value without onChange, and the input becomes read-only: React keeps putting the state back.

Every kind of input

React (JSX)
import { useState } from "react";

function App() {
  const [bio, setBio] = useState("");
  const [course, setCourse] = useState("python");
  const [newsletter, setNewsletter] = useState(true);
  const [level, setLevel] = useState("beginner");

  return (
    <div style={{ display: "grid", gap: 10, maxWidth: 340 }}>
      <textarea value={bio} onChange={(e) => setBio(e.target.value)} placeholder="Short bio" rows={3} />
      <select value={course} onChange={(e) => setCourse(e.target.value)}>
        <option value="python">Python</option>
        <option value="sql">SQL</option>
        <option value="react">React</option>
      </select>
      <label>
        <input type="checkbox" checked={newsletter} onChange={(e) => setNewsletter(e.target.checked)} /> Send me the newsletter
      </label>
      <div>
        {["beginner", "intermediate", "advanced"].map((l) => (
          <label key={l} style={{ marginRight: 12 }}>
            <input type="radio" name="level" value={l} checked={level === l} onChange={(e) => setLevel(e.target.value)} /> {l}
          </label>
        ))}
      </div>
      <pre style={{ background: "#f3f4f8", padding: 10, borderRadius: 8 }}>
        {JSON.stringify({ bio, course, newsletter, level }, null, 2)}
      </pre>
    </div>
  );
}
  • textarea and select use value, just like text inputs. (In plain HTML, a textarea's text goes between its tags and a select uses selected; React makes them all consistent.)
  • Checkboxes use checked, read from e.target.checked.
  • A radio button is checked when its value matches the state: checked={level === l}.

A whole form in one object

React (JSX)
import { useState } from "react";

function App() {
  const [form, setForm] = useState({ name: "", email: "", password: "" });
  const [welcome, setWelcome] = useState("");

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

  const errors = {
    name: form.name.trim() === "" ? "A name is required" : "",
    email: !form.email.includes("@") ? "Enter a valid email" : "",
    password: form.password.length < 8 ? "Use at least 8 characters" : "",
  };
  const valid = Object.values(errors).every((msg) => msg === "");

  function handleSubmit(e) {
    e.preventDefault();
    setWelcome(`Welcome, ${form.name}!`);
  }

  return (
    <form onSubmit={handleSubmit} style={{ display: "grid", gap: 10, maxWidth: 300 }}>
      {["name", "email", "password"].map((field) => (
        <label key={field} style={{ textTransform: "capitalize" }}>
          {field}
          <input
            name={field}
            type={field === "password" ? "password" : "text"}
            value={form[field]}
            onChange={handleChange}
            style={{ display: "block", width: "100%", boxSizing: "border-box" }}
          />
          {form[field] && errors[field] && <small style={{ color: "crimson" }}>{errors[field]}</small>}
        </label>
      ))}
      <button disabled={!valid}>Create account</button>
      {welcome && <p>{welcome}</p>}
    </form>
  );
}
  • Each input's name matches a key in the state object, so one handler serves every field. [name]: value is a computed property name: it uses the variable's value as the key.
  • { ...form, [name]: value } copies the object and replaces one field. Never change a state object in place.
  • The errors are calculated from the state on every render rather than stored, so they can never fall out of date.
  • An error shows only once a field has some text, so an untouched form isn't covered in red.

Uncontrolled inputs with useRef

React (JSX)
import { useRef, useState } from "react";

function App() {
  const fileInput = useRef(null);
  const [info, setInfo] = useState("");

  function check() {
    const file = fileInput.current.files[0];
    setInfo(file ? `${file.name}, ${Math.round(file.size / 1024)} KB` : "No file chosen");
  }

  return (
    <div>
      <input type="file" ref={fileInput} />{" "}
      <button onClick={check}>Check the file</button>
      <p>{info}</p>
    </div>
  );
}

Some inputs can't be controlled at all, like file pickers, and sometimes you only need a value at the moment of submitting. useRef gives you the DOM element itself, as fileInput.current, so you can read it when you need to.

Finished this lesson?Mark it complete to earn +20 XP and move your progress on.