Handlers are props
<button onClick={handleClick}>Save</button>
// HTML was a string of code
<button onclick="handleClick()">Save</button>
onClick={handleClick()} calls the function while rendering and passes its return value as the handler. The symptom is famous: the action happens immediately on load, once per render, and nothing happens on click.
Passing arguments
A new arrow function on every render is fine. It is one of the cheapest things in a render, and worrying about it before measuring is the classic premature optimisation in React.
<button onClick={() => remove(door.id)}>Remove</button>
// the arrow is the handler; remove runs only when it is clicked
// with the event as well:
<button onClick={(e) => remove(door.id, e.shiftKey)}>Remove</button>
Where to define one
function Row({ door, onRemove }) {
// named, for anything longer than a line
function handleRemove(event) {
if (event.shiftKey) return onRemove(door.id, { silent: true });
onRemove(door.id);
}
return (
<li>
{door.name}
<button onClick={handleRemove}>Remove</button>
<button onClick={() => onRemove(door.id)}>Quick</button>
<button onClick={onRemove.bind(null, door.id)}>Also fine, rarer</button>
</li>
);
}
The event object
React passes a SyntheticEvent — a thin wrapper with the same API across browsers. Since React 17 it is no longer pooled, so you can keep a reference to it and read it later.
| Property | What it gives you |
|---|---|
e.target | The element the event started on |
e.currentTarget | The element the handler is attached to — usually what you want |
e.preventDefault() | Stop the browser default: navigating, submitting, scrolling |
e.stopPropagation() | Stop it reaching handlers further up |
e.key, e.shiftKey, e.metaKey | Keyboard and modifiers |
e.nativeEvent | The real DOM event underneath |
preventDefault and stopPropagation
// the browser would follow the link; we handle it ourselves
<a href="/doors" onClick={(e) => { e.preventDefault(); go("/doors"); }}>Doors</a>
// the row opens a detail panel, but the button inside must not open it
<li onClick={openDetail}>
{door.name}
<button onClick={(e) => { e.stopPropagation(); remove(door.id); }}>Remove</button>
</li>
preventDefault cancels what the browser would do. stopPropagation cancels what your other handlers would do. Returning false from a React handler does neither — that was jQuery.
Where React really listens
React does not attach a listener to each element. It attaches one listener per event type to the root container and dispatches from there. This is why handlers are cheap even on a list of a thousand rows.
It also means a handler attached outside React with addEventListener may see an event before React does, and stopPropagation in a native listener on a parent will stop React seeing it at all.
Keyboard and focus
onFocus and onBlur in React bubble (the DOM ones do not), so you can put them on a wrapper to know when focus leaves a whole group.
<input onKeyDown={(e) => { if (e.key === "Enter") save(); }} />
// a div is not a button: it needs role, tabIndex, and keyboard handling
<div role="button" tabIndex={0}
onClick={pick}
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); pick(); } }}>
Pick
</div>
// or just use a button, and get all of that for free
Naming
- The prop a component accepts:
onSomething—onPick,onRemove,onChange. - The function that implements it:
handleSomething—handlePick,handleRemove. - Name the prop after what happened, not what the parent will do with it:
onPick, notonOpenDetailPanel. The child should not know.