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 ine.target.value. Convert it explicitly withNumber(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
| Element | Bind prop | Read in handler | State shape |
|---|---|---|---|
input (text, email, number) | value | e.target.value | string |
textarea | value | e.target.value | string |
select | value | e.target.value | string |
select multiple | value | e.target.selectedOptions | array |
input type="checkbox" | checked | e.target.checked | boolean |
input type="radio" | checked (per option) | e.target.value | string |
Best Practices
- Bind text, select, and number inputs with
value; bind checkboxes withchecked—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 withNumber()before arithmetic, since the event gives you a string. - Wrap each input in a
<label>(or usehtmlFor) 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
useReduceronce a single object handler grows hard to follow.