Forms and controlled inputs
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>
);
}textareaandselectusevalue, just like text inputs. (In plain HTML, a textarea's text goes between its tags and a select usesselected; React makes them all consistent.)- Checkboxes use
checked, read frome.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
namematches a key in the state object, so one handler serves every field.[name]: valueis 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.