A React 19 component attaches its own click listener to a child DOM node with addEventListener via a ref, and the same subtree also has React onClick and onClickCapture props. In what order do these run, and what happens when either side calls stopPropagation?
answer
- ask where each listener physically sits
- container is high, your node is low
- capture and bubble give opposite answers
- stopping below the root kills dispatch
- already-fired listeners cannot be undone
basics
~20 sReact dispatches from a listener on the root container, so a native bubble listener on an inner node runs before React's onClick, while React's onClickCapture runs before native capture listeners on descendants. A native stopPropagation below the root cancels React entirely.
solid answer
~50 sWork it out from where the listeners physically sit. React's listeners are on the root container; your `addEventListener` is on a node deep inside it. During capture, the container is reached first, so **`onClickCapture` runs before** a native capture listener on a descendant. During bubbling, the descendant is reached first, so a **native bubble listener runs before `onClick`** — React has not even been told about the event yet. That asymmetry is the whole answer. It also decides `stopPropagation`: if the native bubble listener stops the event, it never reaches the container and *no* React handler in the app fires for that click. If a React `onClick` stops it, the inner native listener has already run, but listeners above the root container — on `document` or `window` — will not. Mixing the two is a known source of order-dependent bugs; prefer keeping a subtree's handling on one side of the boundary.
code
javascript · 23 linesimport { useEffect, useRef } from 'react';
export function Row() {
const ref = useRef(null);
useEffect(() => {
const el = ref.current;
const onNative = (event) => {
console.log('native bubble listener on the div');
// Uncomment to silence every React handler for this click:
// event.stopPropagation();
};
el.addEventListener('click', onNative);
return () => el.removeEventListener('click', onNative);
}, []);
return (
<div ref={ref} onClick={() => console.log('React onClick on the div')}>
<button onClick={() => console.log('React onClick on the button')}>go</button>
</div>
);
}
// Logs: native bubble listener -> React onClick on the button -> React onClick on the divgo deeper
Know that a hand-written addEventListener and a React onClick are not interchangeable, and that React's listener lives on the root container rather than the element you clicked.
Derive the order from tree position rather than memorising it: the container is reached first going down and last coming up, which is why capture and bubble give opposite answers.
Diagnose it live — recognise the signature of a native stopPropagation below the root killing all React dispatch, and reach for containment checks instead of propagation tricks when integrating third-party DOM code.
Set the boundary policy: which subtrees may own native listeners, that stopPropagation is a reviewed exception rather than a convenience, and how third-party widgets are isolated so ordering never becomes load-bearing across teams.
## Reason from position, not from framework folklore There is no special rule for "React handlers versus native handlers". There is one rule: the browser propagates the event through the real DOM, capture phase downward then bubble phase upward, and every listener runs when the event reaches *its* node. React's listeners are just listeners — they happen to be on the root container, and everything they do afterwards is dispatch, not propagation. So draw the tree: ``` document └── #root <- React's capture + bubble listeners live here └── <div> <- your addEventListener via a ref └── <button> <- event.target ``` ## The resulting order For a click on the button: 1. **Capture, downward.** `document` capture listeners, then `#root` — React's capture listener fires here and immediately dispatches every `onClickCapture` prop it collects along the React tree, outermost-first. Then the browser continues down and your native *capture* listener on the `<div>` fires. 2. **Bubble, upward.** Your native *bubble* listener on the `<div>` fires. Then the event reaches `#root`, React's bubble listener fires, and React dispatches every `onClick` prop along the React tree, innermost-first. Then `document`. Two consequences people find counterintuitive: - **All** React capture handlers — including one on a deeply nested component — run *before* a native capture listener on a node above that component, because React replays the whole capture pass at the container in one go. - A native bubble listener on the immediate parent `<div>` runs *before* the `onClick` on the `<button>` itself, even though the button is the deeper node. React's bubble pass has not started yet. ```javascript import { useEffect, useRef } from 'react'; function Row() { const ref = useRef(null); useEffect(() => { const el = ref.current; const onNative = () => console.log('2. native bubble on the div'); el.addEventListener('click', onNative); return () => el.removeEventListener('click', onNative); }, []); return ( <div ref={ref} onClickCapture={() => console.log('1. onClickCapture')}> <button onClick={() => console.log('3. onClick')}>go</button> </div> ); } ``` ## stopPropagation across the boundary **Native listener stops it (below the root container).** The event never reaches the container, React's listener never fires, and *nothing* is dispatched — not the handler on that subtree, not `onClick` on ancestors, not capture handlers if the stop happened during capture. From inside React it looks as if the framework broke. This is the first thing to check when a click handler mysteriously does nothing in an app that also contains third-party DOM code, a drag library, or a `stopPropagation()` someone added to "contain" a click. **React handler stops it.** Calling `stopPropagation()` on the synthetic event stops the underlying propagation from continuing past the root container, so listeners on `document` or `window` do not run. But your native listener on the inner `<div>` already ran — it fired before React was ever involved. You cannot retroactively cancel it. The practical version: a dropdown that closes on outside clicks via `document.addEventListener('click', close)` stops closing the moment someone adds `stopPropagation()` to a React `onClick` inside the menu, because that document listener is above the root container. Detect outside clicks with a containment check (`ref.current.contains(event.target)`) or a capture-phase listener rather than relying on propagation reaching `document`. ## Ordering among listeners on the same node If you attach a native listener to the root container itself, ordering against React's listener on that node is decided by registration order within the phase — which depends on when your code ran relative to root creation. That is fragile by construction; do not build behaviour on it. ## What to actually do - **Pick one side per subtree.** Handling the same event both natively and through props in the same region is the failure mode; the ordering rules are learnable but nobody re-derives them during a bug hunt. - **When you must go native** — because a third-party widget owns the node, or you need a non-passive listener for `preventDefault` on a scroll-blocking touch event — attach on the specific node, document the ordering you rely on, and clean up in the effect's teardown. - **Prefer containment checks over propagation tricks.** `element.contains(event.target)` does not care which side of the boundary handled the event first. - **Be suspicious of every stopPropagation in a React codebase.** It is a global claim about a subtree made from one component, and it is exactly what breaks native listeners above and below. ## Saying it in an interview "React's listeners are on the root container, so during capture React runs first and during bubbling it runs last. A native bubble listener on a child therefore beats `onClick`, and if it stops propagation React never dispatches at all. React's own `stopPropagation` stops listeners above the root — `document`, `window` — but not ones that already fired inside."
- A click handler in one component suddenly stops firing after a third-party drag library is added. How do you confirm the cause quickly?Attach a temporary capture-phase listener on the root container and log the event. If the capture listener sees the click but nothing dispatches on the way up, something between the target and the container is stopping bubbling — the library's own listener. From there, inspect listeners on the intermediate nodes in DevTools and either configure the library, move the handling to capture-phase React props, or handle the event natively on the same node.
- Why does a React onClickCapture run before a native capture listener on a child component's own DOM node?Because React replays the entire capture pass at the root container. The browser reaches the container first on the way down, React fires its capture listener there and immediately walks the React tree calling every `onClickCapture` it finds, outermost-first — all before the browser continues descending to the child node where your native capture listener is registered.
- An outside-click dropdown that listens on document stops working after someone adds stopPropagation to a React handler inside the menu. What is the durable fix?Stop depending on propagation reaching `document`. Use a containment check instead — on each document click, close only when `menuRef.current` and the trigger's ref both fail `contains(event.target)` — or register the document listener in the capture phase so it runs before React dispatches at all. Both are immune to whatever any inner handler does with propagation.
- If you attach a native listener to the root container itself, does it run before or after React's listener on that node?It depends on registration order within that phase — whichever listener was added first runs first, so the answer changes with whether your code executed before or after `createRoot`. Treat that as undefined behaviour and never build logic on it; attach to a specific inner node instead, where the tree position gives a deterministic answer.
saying these in an interview costs you the question
- Assumes React handlers always run before native listeners
- Thinks stopPropagation in React cancels listeners that already fired
- Says a native stopPropagation inside the root cannot affect React
- Believes capture and bubble ordering across the boundary are symmetric
- Attaches listeners to the root container and relies on the order