Event handlers
Respond to user interaction with camelCased props such as onClick and onChange.
<button onClick={(e) => { /* ... */ }} />Parameters
| Parameter | Type | Description |
|---|---|---|
$onClick, onChange, onSubmit, …optional | function | A function React calls when the event fires. Names are camelCased; most have a Capture variant such as onClickCapture that runs during the capture phase — onMouseEnter and onMouseLeave are the exceptions, since they have no capture-phase variant. |
$e | React event object | The synthetic event passed to the handler. Carries target, currentTarget, bubbles, defaultPrevented, timeStamp and nativeEvent, and the methods preventDefault(), stopPropagation(), isDefaultPrevented() and isPropagationStopped(). |
Errors
React throws if an event prop is given something that is not a function, for example onClick="handleClick" as a string.
Changelog
- PHP 17.0Handlers are attached to the root container rather than to document, which changes how React events interleave with listeners added outside React.
- PHP 17.0onScroll no longer bubbles; onFocus and onBlur switched to the native focusin/focusout events, so they do bubble.
- PHP 17.0Event pooling was removed — you no longer need e.persist() to read the event asynchronously.
Event handlers are props whose value is a function. React attaches one listener at the root of your app and dispatches through the component tree, so handlers appear to behave like native ones while staying declarative.
Passing versus calling
The single most common bug: onClick={handleClick} passes the function, onClick={handleClick()} calls it during render and passes the result. If you need arguments, wrap it — onClick={() => remove(id)}.
The event object
Handlers receive a synthetic event, conventionally e. e.target is where the event started, e.currentTarget is the element the handler sits on — the difference matters when you are catching events from children. e.preventDefault() suppresses the browser's default action, which is how you stop a form navigating; e.stopPropagation() stops the event travelling further up. They are unrelated, and mixing them up is a frequent cause of a form that still reloads the page.
What trips people up
Every event propagates except onScroll, so a click handler on a wrapper also fires for clicks on its buttons. onChange in React fires on every keystroke, unlike the DOM event of the same name; there is no need to reach for onInput.
Example
When to use it
- Incrementing a counter or toggling a panel on click
- Keeping a controlled input in state as the user types
- Submitting a form with fetch instead of a page navigation
- Closing a dropdown when a click lands outside it, without swallowing inner clicks
Frequently asked questions
Why does my onClick fire immediately when the page loads?
Because the handler is being called rather than passed: onClick={doThing()} runs during render. Pass the reference, onClick={doThing}, or wrap arguments in an arrow function, onClick={() => doThing(id)}.
How do I stop a form from reloading the page?
Call e.preventDefault() as the first line of the onSubmit handler on the <form> element. e.stopPropagation() is a different thing and will not help — it only stops the event reaching ancestor handlers.
Why does clicking a child also trigger the parent's handler?
Because React events propagate up the tree just like DOM events; onScroll is the only exception. Call e.stopPropagation() in the child's handler, or check e.target before acting in the parent's.
More examples
target versus currentTarget
One handler serves every row because the event bubbles up from the cells. currentTarget stays pinned to the tr that owns the handler, so the row id is always correct, while target tells you which descendant was hit. If Delete should not also select the row, the button's own handler calls e.stopPropagation().
const ROWS = [
{ id: 1, name: "Invoice A" },
{ id: 2, name: "Invoice B" }
];
export default function Table() {
function handleClick(e) {
// currentTarget is always the <tr> the handler is on.
const rowId = e.currentTarget.dataset.id;
// target is whatever was actually clicked: the row, or the button inside it.
const wasButton = e.target.tagName === "BUTTON";
console.log(rowId, wasButton ? "button clicked" : "row clicked");
}
return (
<table>
<tbody>
{ROWS.map((r) => (
<tr key={r.id} data-id={r.id} onClick={handleClick}>
<td>{r.name}</td>
<td>
<button>Delete</button>
</td>
</tr>
))}
</tbody>
</table>
);
}A form handler that reads every field
preventDefault stops the browser navigating, and FormData reads the uncontrolled fields by their name attributes, so no onChange handler or piece of state is needed per input. e.currentTarget is the form element itself. Submitting also happens when Enter is pressed in a text field, which a click handler on the button would miss.
export default function ContactForm() {
function handleSubmit(e) {
e.preventDefault();
const data = new FormData(e.currentTarget);
const values = Object.fromEntries(data);
// { name: "Ada", topic: "billing" } — no state needed per field
console.log(values);
}
return (
<form onSubmit={handleSubmit}>
<input name="name" defaultValue="Ada" />
<select name="topic" defaultValue="billing">
<option value="billing">Billing</option>
<option value="bug">Bug report</option>
</select>
<button type="submit">Send</button>
</form>
);
}
Discussion