Events, Lists and Forms
Handle events, render lists with keys and build controlled forms.
What you will learn
- Handle events
- Render lists with keys
- Build a controlled form
Interfaces are interactive: users click, type and submit. In React you attach handlers with camelCase props such as onClick, onChange and onSubmit, and pass them a function, not the result of calling one.
function Save() {
function handleClick(event) {
console.log("saved", event.type);
}
return <button onClick={handleClick}>Save</button>;
// Wrong: onClick={handleClick()} runs immediately on every render
}Rendering lists
Use map to turn an array into elements. Each item needs a stable, unique key prop so React can tell which item is which when the list changes. Use an id from your data, not the array index, if the list can be reordered or filtered.
const todos = [
{ id: 1, text: "Learn React", done: true },
{ id: 2, text: "Build an app", done: false },
];
function TodoList() {
return (
<ul>
{todos.map((t) => (
<li key={t.id} style={{ textDecoration: t.done ? "line-through" : "none" }}>
{t.text}
</li>
))}
</ul>
);
}Controlled inputs
In a controlled input, React state is the single source of truth: the input shows value from state and updates it in onChange. That makes validation, formatting and clearing trivial.
function NameForm() {
const [name, setName] = useState("");
return (
<label>
Name
<input value={name} onChange={(e) => setName(e.target.value)} />
<p>Hello, {name || "stranger"}</p>
</label>
);
}function AddTodo({ onAdd }) {
const [text, setText] = useState("");
function handleSubmit(e) {
e.preventDefault(); // stop the page reload
if (!text.trim()) return;
onAdd(text.trim());
setText("");
}
return (
<form onSubmit={handleSubmit}>
<input value={text} onChange={(e) => setText(e.target.value)} placeholder="New task" />
<button type="submit">Add</button>
</form>
);
}
function App() {
const [todos, setTodos] = useState([]);
return (
<>
<AddTodo onAdd={(t) => setTodos([...todos, { id: crypto.randomUUID(), text: t }])} />
<ul>{todos.map((t) => <li key={t.id}>{t.text}</li>)}</ul>
</>
);
}Checkboxes use checked and e.target.checked; selects use value like text inputs. For forms with many fields, store them in one object: setForm({ ...form, [e.target.name]: e.target.value }).
Forgetting preventDefault() reloads the page. Missing keys cause a console warning and odd behavior. An input with value but no onChange is read-only.
Try it yourself
Extend the todo app: add a checkbox to mark tasks done, and a delete button per task. Update state without mutating it.
Show solution
const toggle = (id) =>
setTodos(todos.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
const remove = (id) => setTodos(todos.filter((t) => t.id !== id));
<li key={t.id}>
<input type="checkbox" checked={!!t.done} onChange={() => toggle(t.id)} />
{t.text}
<button onClick={() => remove(t.id)}>Delete</button>
</li>