Runnable lesson · press Try it on the examples to edit and run them here.
The click is the event you will use most. This lesson puts the earlier ideas together in practical examples.
A counter with three buttons
function App() {
const [count, setCount] = React.useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount((c) => c + 1)}>+1</button>{" "}
<button onClick={() => setCount((c) => c - 1)}>-1</button>{" "}
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}
A toggle
Toggle with the opposite of the current value. Using the updater form means rapid clicks never get out of step:
function App() {
const [open, setOpen] = React.useState(false);
return (
<div>
<button onClick={() => setOpen((o) => !o)} aria-expanded={open}>
{open ? "Hide details" : "Show details"}
</button>
{open ? <p>Here are the details</p> : <p>Details are hidden</p>}
</div>
);
}
aria-expanded tells screen readers whether the content is open.
Clicks in a list
Each item needs to know which item was clicked. Close over the item (or its id) in an arrow function:
function App() {
const products = [
{ id: 1, name: "Pen" },
{ id: 2, name: "Book" },
];
const [bought, setBought] = React.useState(0);
return (
<div>
<p>Bought: {bought}</p>
{products.map((p) => (
<button key={p.id} onClick={() => setBought(bought + 1)}>Buy {p.name}</button>
))}
</div>
);
}
Clicking a link without leaving the page
A link normally navigates. Call preventDefault() on the event to stop that, and think about whether a <button> is the better element:
function App() {
const [msg, setMsg] = React.useState("Not clicked");
function handleClick(e) {
e.preventDefault();
setMsg("Link clicked, page stayed");
}
return (
<div>
<a href="https://example.com" onClick={handleClick}>Run action</a>
<p>{msg}</p>
</div>
);
}
If the thing you click performs an action rather than going somewhere, use <button type="button"> instead. Use real links for real navigation.
Clicks that should not bubble
A click on a child element also triggers click handlers on its parents (it bubbles). Stop that with e.stopPropagation():
function App() {
const [parent, setParent] = React.useState(0);
return (
<div>
<div onClick={() => setParent(parent + 1)} role="button" tabIndex={0} aria-label="Parent area" style={{ padding: 16, background: "#eee" }}>
Parent area
<button onClick={(e) => e.stopPropagation()}>Inner button</button>
</div>
<p>Parent clicks: {parent}</p>
</div>
);
}
Use stopPropagation sparingly; it can hide events from code that expects them. Bubbling is described in Event Bubbling.
Double clicks, right clicks and more
onDoubleClick, onContextMenu (right-click), onMouseDown, onMouseUp and onPointerDown exist too. Prefer onClick for buttons, because it also works with the keyboard (Enter and Space).
Disabled buttons and double submission
A button that triggers a slow action should be disabled while it works, so a second click cannot start it twice:
function App() {
const [status, setStatus] = React.useState("idle");
function handleSave() {
setStatus("saving");
setTimeout(() => setStatus("saved"), 1000);
}
return (
<div>
<button onClick={handleSave} disabled={status === "saving"}>
{status === "saving" ? "Saving..." : "Save"}
</button>
{status === "saved" && <p>Saved</p>}
</div>
);
}
Key points
- Use
onClickon<button>for actions; use real links for navigation. - Use the updater form for toggles and counters.
preventDefaultstops the default action;stopPropagationstops bubbling.
Exercise: A like button
Make a button that shows “Like” and toggles to “Liked” when clicked. Show the number of likes, which should go up by one when liked and down by one when un-liked. —solution
function App() {
const [liked, setLiked] = React.useState(false);
const likes = liked ? 1 : 0; // derived from state, no second state needed
return (
<div>
<button onClick={() => setLiked((l) => !l)}>{liked ? "Liked" : "Like"}</button>
<p>Likes: {likes}</p>
</div>
);
}
The number of likes can be calculated from liked, so it is not a second piece of state.