Skip to content
React rc forms 4 min read

Inputs, Selects & Checkboxes

Every form element in HTML stores its own value, but React apps usually want that value in state so it can be validated, transformed, and displayed live. Wiring an element to state is called making it controlled, and each element type does it slightly differently: text inputs use value, checkboxes use checked, selects can be single or multiple, and radios share one piece of state across several inputs. This page walks through the controlled pattern for each type and shows how to drive many fields from a single handler.

Text inputs and textareas

A controlled text input reads its value from state and writes back through onChange on every keystroke. The same pattern applies to <textarea>—in React it takes a value prop rather than children, which keeps it consistent with every other input.

import { useState } from "react";

function Profile() {
  const [name, setName] = useState("");
  const [bio, setBio] = useState("");

  return (
    <form>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Your name"
      />
      <textarea
        value={bio}
        onChange={(e) => setBio(e.target.value)}
        rows={4}
      />
      <p>Hello, {name || "stranger"}{bio.length} chars in bio</p>
    </form>
  );
}

Output:

Hello, Ada — 27 chars in bio

Number inputs (type="number") still hand you a string in e.target.value. Convert it explicitly with Number(e.target.value) before doing math, or you will silently concatenate instead of add.

Select dropdowns

In plain HTML you mark the chosen <option> with a selected attribute. React ignores that—instead you set value on the <select> element itself, which keeps all the binding logic in one place.

import { useState } from "react";

function PlanPicker() {
  const [plan, setPlan] = useState("pro");

  return (
    <select value={plan} onChange={(e) => setPlan(e.target.value)}>
      <option value="free">Free</option>
      <option value="pro">Pro</option>
      <option value="team">Team</option>
    </select>
  );
}

Multiple selects

Add the multiple attribute and the state becomes an array. The selected values live in e.target.selectedOptions, a live collection you spread and map over.

import { useState } from "react";

function SkillPicker() {
  const [skills, setSkills] = useState(["react"]);

  function handleChange(e) {
    const chosen = [...e.target.selectedOptions].map((o) => o.value);
    setSkills(chosen);
  }

  return (
    <select multiple value={skills} onChange={handleChange}>
      <option value="react">React</option>
      <option value="node">Node</option>
      <option value="sql">SQL</option>
    </select>
  );
}

Checkboxes

A checkbox is a boolean, so it binds to checked instead of value, and you read e.target.checked in the handler. Confusing the two is the most common form bug in React—value does nothing useful on a checkbox.

import { useState } from "react";

function TermsBox() {
  const [agreed, setAgreed] = useState(false);

  return (
    <label>
      <input
        type="checkbox"
        checked={agreed}
        onChange={(e) => setAgreed(e.target.checked)}
      />
      I accept the terms
    </label>
  );
}

Groups of checkboxes

When several checkboxes feed one list, store an array and add or remove each value as it toggles.

import { useState } from "react";

const TOPICS = ["news", "deals", "events"];

function Subscriptions() {
  const [picked, setPicked] = useState([]);

  function toggle(topic) {
    setPicked((prev) =>
      prev.includes(topic)
        ? prev.filter((t) => t !== topic)
        : [...prev, topic]
    );
  }

  return TOPICS.map((topic) => (
    <label key={topic}>
      <input
        type="checkbox"
        checked={picked.includes(topic)}
        onChange={() => toggle(topic)}
      />
      {topic}
    </label>
  ));
}

Radio groups

Radios in the same group share a name, and only one can be active at a time. In React you model that with a single state variable; each radio is checked when its value equals the state.

import { useState } from "react";

function ShippingChoice() {
  const [speed, setSpeed] = useState("standard");

  return (
    <fieldset>
      {["standard", "express", "overnight"].map((opt) => (
        <label key={opt}>
          <input
            type="radio"
            name="speed"
            value={opt}
            checked={speed === opt}
            onChange={(e) => setSpeed(e.target.value)}
          />
          {opt}
        </label>
      ))}
    </fieldset>
  );
}

One handler for many fields

Writing a separate handler per field gets tedious fast. Give each input a name that matches a key in a state object, then use one handler that branches on e.target.type to pick the right property.

import { useState } from "react";

function AccountForm() {
  const [form, setForm] = useState({
    username: "",
    role: "viewer",
    newsletter: false,
  });

  function handleChange(e) {
    const { name, type, value, checked } = e.target;
    setForm((prev) => ({
      ...prev,
      [name]: type === "checkbox" ? checked : value,
    }));
  }

  return (
    <form>
      <input name="username" value={form.username} onChange={handleChange} />
      <select name="role" value={form.role} onChange={handleChange}>
        <option value="viewer">Viewer</option>
        <option value="editor">Editor</option>
      </select>
      <label>
        <input
          name="newsletter"
          type="checkbox"
          checked={form.newsletter}
          onChange={handleChange}
        />
        Subscribe
      </label>
    </form>
  );
}

Output:

{ username: "ada", role: "editor", newsletter: true }

The trick is the computed key [name] plus the type === "checkbox" check—that single line correctly handles text, selects, and checkboxes from one place.

Quick reference

ElementBind propRead in handlerState shape
input (text, email, number)valuee.target.valuestring
textareavaluee.target.valuestring
selectvaluee.target.valuestring
select multiplevaluee.target.selectedOptionsarray
input type="checkbox"checkede.target.checkedboolean
input type="radio"checked (per option)e.target.valuestring

Best Practices

  • Bind text, select, and number inputs with value; bind checkboxes with checked—never mix them up.
  • Initialize controlled inputs with a defined value ("", false, []) so React never warns about switching from uncontrolled to controlled.
  • Convert type="number" values with Number() before arithmetic, since the event gives you a string.
  • Wrap each input in a <label> (or use htmlFor) so clicks and screen readers target the right control.
  • Use one name-driven handler for large forms to avoid a wall of nearly identical callbacks.
  • Reach for useReducer once a single object handler grows hard to follow.
Last updated June 14, 2026
Was this helpful?