skip to content

In React 19, when you write <button onClick={handleClick}>, does React call addEventListener on that button DOM node? Explain what React does instead and how your handler ends up running.

level: juniorimportance: should knowfreq 45%

answer

  1. not one listener per element
  2. registered where the root mounts
  3. target is looked up, not listened to
  4. walks the fiber parent chain
  5. props collected, then replayed

basics

~10 s

No. React registers listeners once on the root container passed to createRoot, then dispatches a click by finding the target's component and walking up the component tree, calling the onClick props it collects.

solid answer

~40 s

React does not attach a DOM listener per element. When you create a root with `createRoot(container)`, React registers listeners on that container element for the event types it supports, in both the capture and bubble phases — once per root, not once per element or per render. Your `onClick` prop is just data stored on that element's fiber. When a real click bubbles up to the container, React's listener runs, looks up the fiber for `event.target`, walks that fiber's parent chain to the root, collects the `onClickCapture` and `onClick` props it finds, and invokes them in capture-then-bubble order with a single synthetic event. That is why rendering ten thousand clickable rows adds zero DOM listeners, and why DevTools shows no click listener on the button itself.

go deeper

for a junior

Be ready to say plainly that onClick is not a DOM listener on that element: React attaches listeners at the root container and dispatches from there. Knowing where the listener lives is the whole point of the question.

for a middle

Explain the dispatch path concretely — target node to fiber, walk the parent chain, collect capture and bubble props, call them in order — and name the cost it saves on large, frequently remounted lists.

for a senior

Show what the design implies operationally: a stray native listener that stops propagation before the root container kills React handlers wholesale, and non-bubbling events such as scroll and media events are attached directly instead.

for a principal

Frame it as a tradeoff the framework made on your behalf: one dispatch chokepoint buys batching, priority tagging and cheap list churn, at the cost of interop surprises whenever non-React code touches the same DOM subtree. Say how you would fence that in a codebase.

## The one-line answer No — `addEventListener('click', ...)` is never called on your `<button>`. React installs a small fixed set of listeners on the root container and does the rest itself. What looks like "a handler on this button" is a prop stored in React's internal tree, plus a dispatch algorithm. ## What React installs, and when When you call `createRoot(container)` or `hydrateRoot(container, ui)`, React registers listeners on that container element for the event types it supports, in **both** the capture phase and the bubble phase. That registration happens once, at root creation. It does not scale with your UI: ```javascript const root = createRoot(document.getElementById('root')); root.render(<App />); // listeners live on #root, not on anything inside it ``` Rendering one button or ten thousand makes no difference to the number of DOM listeners. An `onClick` prop is stored on the fiber — React's internal object for that element — the same way `className` or `id` is stored, except React never writes it to the DOM. ## How a click becomes your handler 1. The browser dispatches the native click normally: capture from `window` down to the target, then bubble back up. React is not involved yet. 2. On the way down, the event passes through the root container, and React's capture listener there runs. On the way up, the event reaches the root container again, and React's bubble listener runs. 3. Inside that listener, React takes `event.target` and finds the fiber for it — every DOM node React created carries an internal pointer back to its fiber. 4. React walks that fiber's parent chain (the *return path*) up to the root, collecting the relevant props from each component along the way. 5. React builds one synthetic event and calls the collected handlers in order: capture-phase props outermost-first, bubble-phase props innermost-first. So the propagation you *observe* in React is a replay React performs over the component tree. The browser only ever propagated the event as far as the root container. ## Why do it this way **Cost.** Attaching and detaching a listener per node is real work, and lists that mount and unmount constantly pay it repeatedly. One listener per event type per root removes that churn entirely, and removes the matching memory retention when nodes are discarded. **Consistency.** React normalizes event names and behaviour across browsers in one place instead of at every call site. **Control.** Because every event enters React through one door, React can tag the resulting state update with a priority derived from the kind of event that caused it, and it can batch all updates from that one dispatch together. ## What follows in practice - Inspecting the button in DevTools' event-listeners panel shows nothing for click; the listener is on the root container. - **Anything that stops propagation between the target and the root container silences React handlers completely.** A hand-written listener on an intermediate node that calls `stopPropagation()` means the event never reaches the container, so React never dispatches. This is the single most common React/native interop bug. - Conversely, elements React did not render are untouched. React only dispatches to props it finds on fibers. - Handler identity does not matter for attachment cost: passing a fresh arrow function every render does not cause any `addEventListener`/`removeEventListener` traffic, because there was never a DOM listener to swap. ## The exceptions Delegation only works for events that actually propagate, so React attaches the non-bubbling ones directly to the element instead. Media events such as `onPlay` and `onEnded`, `onLoad` and `onError` on `<img>`, and `onScroll` are handled that way — and since React 17, `onScroll` deliberately does not bubble through the React tree either, matching the DOM. Portals are the other special case: content rendered through `createPortal` lives in a DOM node outside the root container, so React registers its listeners on the portal's container element too. That is what lets a click inside a portal still find its way into React's dispatch. ## Saying it in an interview "React uses delegation at the root container. `onClick` isn't a DOM listener, it's a prop React reads during dispatch: the browser event reaches the container, React finds the fiber for the target, walks up the component tree collecting handlers, and calls them. One listener per event type, no matter how many elements."

  • Which events does React not delegate at the root, and why?
    Ones that do not propagate. Media events such as `onPlay` and `onEnded`, plus `onLoad`/`onError` on images and `onScroll`, are attached directly to the element, because there is nothing for a container-level listener to catch. Since React 17, `onScroll` also does not bubble through the React tree, which matches the DOM and stops parent components seeing every child's scrolling.
  • If the listener is on the root container, how does React know which handlers to run and in what order?
    Every DOM node React creates carries an internal reference to its fiber. React reads `event.target`, finds that fiber, and walks its parent chain up to the root, collecting `onClickCapture` props for the capture pass and `onClick` props for the bubble pass. It then calls capture handlers outermost-first and bubble handlers innermost-first, so the ordering matches what the DOM would have produced.
  • Does passing a new inline arrow function on every render cause listener churn in the DOM?
    No. There is no DOM listener bound to that element to remove and re-add — the function is just a value on the fiber, and React reads whichever one is current when it dispatches. Whether that new function costs anything at all is a re-render question, not an event-attachment one.

It is a building with one receptionist at the door rather than a doorbell on every office. Visitors are logged once at the entrance, and reception routes them along the org chart — not along the corridors.

saying these in an interview costs you the question

  • Says React calls addEventListener on every element with an onClick prop
  • Claims React still attaches its listeners to document in React 19
  • Thinks React copies your handler function onto the DOM node
  • Believes delegation means React only supports one handler per event type
  • Cannot say where the listener actually lives when asked

context