Skip to main content

Responding to Events

An event handler is just a function I hand to a JSX attribute like onClick. The whole topic hinges on one word. I pass the function, I don't call it. After that it's mostly plumbing plus the DOM event model.

Adding event handlers

The pattern is simple. Define a function, then pass it as a prop to a JSX tag. By convention handler functions are named handle followed by the event, like handleClick.

Button.jsx
function Button() {
function handleClick() {
alert("You clicked me!");
}
return <button onClick={handleClick}>Click me</button>;
}

Handlers can be defined separately (as above) or written inline, usually as an arrow function:

inline-handlers.jsx
<button onClick={() => alert("You clicked me!")}>Click me</button>

Pitfall: pass the function, don't call it.

pass-vs-call.jsx
<button onClick={handleClick}>     // ✅ passed, runs on click
<button onClick={handleClick()}> // ❌ called now, during render

The () runs handleClick while rendering and hands its return value to onClick. If that handler sets state, you get an infinite render loop. The same thing happens with onClick={alert('hi')}, which fires on every render. When you need to pass an argument, wrap it in an arrow so the call is deferred: onClick={() => handleClick(id)}. Now the arrow is what runs on click, and it calls the function.

Reading props in event handlers

A handler declared inside a component closes over that component's scope, so it can read props and state directly. No wiring needed.

AlertButton.jsx
function AlertButton({ message, children }) {
return <button onClick={() => alert(message)}>{children}</button>;
}

Passing event handlers as props

Often a child like a generic Button shouldn't decide what a click does. The parent should. So the parent passes the handler down as a prop, and the child just wires it to the DOM event.

PlayButton.jsx
function Button({ onClick, children }) {
return <button onClick={onClick}>{children}</button>;
}

function PlayButton({ movieName }) {
return <Button onClick={() => alert(`Playing ${movieName}`)}>Play</Button>;
}

Naming event handler props

Built-in tags like <button> only accept browser event names like onClick. For my own components I can name the prop whatever fits the app. The convention is on followed by a capital letter, named after the action rather than the event, like onPlayMovie or onUploadImage. That way the implementation can change from a click to a keypress later without touching the parent.

Toolbar.jsx
function Button({ onSmash, children }) {
return <button onClick={onSmash}>{children}</button>;
}

Note: use the right HTML tag. Handle clicks with a real <button onClick={…}>, not <div onClick={…}>. A genuine <button> gives you keyboard navigation and focus for free. If you don't like its default look, restyle it with CSS. Don't downgrade to a <div> and lose the accessibility.

Event propagation

Events propagate. After firing on an element, a React event travels up the tree, running matching handlers on every parent along the way. Both run, innermost first.

bubbling.jsx
<div onClick={() => alert("toolbar")}>
<button onClick={() => alert("play")}>Play</button>
</div>
// clicking the button alerts "play" THEN "toolbar"

Pitfall. All events propagate in React except onScroll, which only fires on the tag it's attached to.

Stopping propagation

Every handler receives the event object e as its first argument. Call e.stopPropagation() to stop the climb, so parent handlers don't fire.

stop-propagation.jsx
function Button({ onClick, children }) {
return (
<button onClick={(e) => {
e.stopPropagation(); // parent's onClick won't run
onClick();
}}>
{children}
</button>
);
}

Capture phase events

Deep dive. Rarely, I need to catch an event on a child even if it called stopPropagation(), for example logging every click to analytics no matter what. Adding Capture to the event name does it:

capture.jsx
<div onClickCapture={() => { /* runs first */ }}>
<button onClick={(e) => e.stopPropagation()} />
</div>

Every event runs in three phases. First it travels down, firing all onClickCapture handlers. Then it runs the clicked element's own onClick. Finally it travels back up, firing parent onClick handlers. Capture is for routers and analytics, not everyday app code.

Writing N handlers doesn't mean N listeners. Putting onClick on 1,000 list items looks like 1,000 DOM listeners. It isn't. React optimizes this under the hood.

looks-like-1000.jsx
{items.map((item) => (
<li key={item.id} onClick={() => select(item.id)}>{item.name}</li>
))}

How it optimizes: React attaches one real listener at the app root and lets browser bubbling carry every event up to it. When an event arrives, React looks at where it came from, walks its internal tree back up, and calls the onClicks I defined along the way. So my JSX handlers are really just entries in React's data, not real DOM listeners. One listener does the work of all of them.

So is hand-rolling delegation worth it? In normal React code, no. React already delegates, so a manual e.target / data-* handler saves no listeners and just adds pain. e.target is whatever you actually clicked (an inner icon, not the row, so now you need closest('[data-id]')), and data-* values are always strings, so you lose the real object and the TS types the clean version keeps.

It only earns its place when I can't put the handler on the element, or when N handlers genuinely hurt. The first case is injected or third-party HTML (dangerouslySetInnerHTML, an embedded widget), where React can't attach handlers to nodes it didn't create, so one delegated handler on the wrapper is the only way in. The second is huge lists (10k+ rows) where a fresh () => select(item.id) closure per row actually shows up in profiling, and one parent handler avoids creating N of them.

Rule of thumb: if I can write onClick on the element, I do. Manual delegation is only for when the handler can't live there, or a scale where the closures measurably bite.

Passing handlers as an alternative to propagation

Instead of leaning on bubbling, I can have the child handler explicitly call the prop it was given. Notice this handler does a bit of its own work first, then calls onClick:

explicit-call.jsx
function Button({ onClick, children }) {
return (
<button onClick={(e) => {
e.stopPropagation();
onClick(); // explicitly invoke the parent's handler
}}>
{children}
</button>
);
}

The payoff is traceability. I can follow the exact chain of handlers that runs, instead of reasoning about what bubbles where.

Preventing default behavior

Some events carry a built-in browser action. The classic one is that submitting a <form> reloads the page. e.preventDefault() stops that so I can handle it in JS.

Signup.jsx
<form onSubmit={(e) => {
e.preventDefault(); // don't reload the page
alert("submitting");
}}>
<input />
<button>Send</button>
</form>

Two names that look alike but are unrelated. e.stopPropagation() stops the event bubbling to parents. e.preventDefault() stops the browser's default reaction. Different jobs, no connection.

Can event handlers have side effects?

Absolutely, and this is the whole point of them. Render functions must stay pure, but event handlers don't have to be. They're the designed place to change things: update state, call an API, write to storage. They don't run during render, they run in response to a user action. Render calculates, handlers act. And to remember something across renders, the handler stores it in state.