Forms
Controlled inputs, multiple fields, validation, and submitting.
Controlled inputs
In plain HTML the input element holds its own value. In React you usually put that value in state and let the input read from it, so there is a single source of truth.
function NameField() {
const [name, setName] = useState("");
return (
<input
value={name}
onChange={event => setName(event.target.value)}
/>
);
}Two things are connected: value pushes state into the input, and onChange pushes typing back into state. Omit the handler and the field appears frozen, because state never changes.
Start text state as an empty string, not undefined. Switching a field from undefined to a value turns an uncontrolled input into a controlled one and React warns about it.
Other input types
Checkboxes use checked instead of value. Selects and textareas both use value, unlike their HTML equivalents.
<input
type="checkbox"
checked={subscribed}
onChange={e => setSubscribed(e.target.checked)}
/>
<textarea value={bio} onChange={e => setBio(e.target.value)} />
<select value={size} onChange={e => setSize(e.target.value)}>
<option value="">Choose a size</option>
<option value="s">Small</option>
<option value="m">Medium</option>
<option value="l">Large</option>
</select>Several fields in one object
A separate state variable per field is fine and often clearest. For larger forms, one object with a shared handler cuts the repetition.
function SignupForm() {
const [form, setForm] = useState({ name: "", email: "", plan: "free" });
function handleChange(event) {
const { name, value } = event.target;
setForm(prev => ({ ...prev, [name]: value }));
}
return (
<>
<input name="name" value={form.name} onChange={handleChange} />
<input name="email" value={form.email} onChange={handleChange} />
<select name="plan" value={form.plan} onChange={handleChange}>
<option value="free">Free</option>
<option value="pro">Pro</option>
</select>
</>
);
}The square brackets in [name]: value are a computed property name — the key comes from the input's name attribute at runtime.
Submitting
function ContactForm() {
const [message, setMessage] = useState("");
const [sending, setSending] = useState(false);
async function handleSubmit(event) {
event.preventDefault();
setSending(true);
try {
await fetch("/api/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message }),
});
setMessage("");
} finally {
setSending(false);
}
}
return (
<form onSubmit={handleSubmit}>
<textarea value={message} onChange={e => setMessage(e.target.value)} />
<button type="submit" disabled={sending}>
{sending ? "Sending…" : "Send message"}
</button>
</form>
);
}Put the handler on the form rather than the button, so pressing Enter in a field submits too.
Validation
function EmailField() {
const [email, setEmail] = useState("");
const [touched, setTouched] = useState(false);
const invalid = touched && !email.includes("@");
return (
<div>
<label htmlFor="email">Email</label>
<input
id="email"
value={email}
onChange={e => setEmail(e.target.value)}
onBlur={() => setTouched(true)}
aria-invalid={invalid}
aria-describedby={invalid ? "email-error" : undefined}
/>
{invalid && (
<p id="email-error">Enter an email address that includes an @.</p>
)}
</div>
);
}Waiting for onBlur before showing an error avoids scolding someone halfway through typing. Deriving invalid during render rather than storing it in state keeps it from going stale.
Uncontrolled inputs
Sometimes you do not need the value until submit. Leave the input alone and read it from a ref, covered in chapter 14.
function QuickSearch() {
const inputRef = useRef(null);
function handleSubmit(event) {
event.preventDefault();
console.log(inputRef.current.value);
}
return (
<form onSubmit={handleSubmit}>
<input ref={inputRef} defaultValue="" />
<button>Search</button>
</form>
);
}Use defaultValue rather than value for an uncontrolled field. Controlled inputs are the default choice; uncontrolled ones are useful for simple or very large forms where re-rendering on every keystroke is wasteful.