Skip to solution
mediumFrontend

How does React handle events?

958 views
01

Understand the problem

Question presented to candidate: "You write onClick on a thousand list rows. How many DOM listeners does React attach?"

What a strong answer should cover:

  • Not one per element. React uses event delegation: it attaches a small number of listeners at the root container and works out which components should receive each event.
  • Since React 17 those listeners are on the root container you passed to createRoot, not on document — which is what makes multiple React versions or micro-frontends on one page safe.
  • From that one native event React reconstructs both phases, so onClickCapture handlers run top-down and onClick handlers run bottom-up, exactly like the DOM.
  • The observable consequence: a native stopPropagation on the element itself prevents the React handler from ever running, because the event has to reach the container first.
  • Conversely, stopPropagation inside a React handler does stop native listeners above the container, since React forwards it to the native event.
  • Delegation is why adding handlers to a long list is cheap, and why handler identity in JSX does not create or remove DOM listeners.
  • Mixing React handlers with manually attached native listeners on the same subtree is where ordering surprises come from.

Clarifying questions expected:

  • "Are we mixing in any manually attached native listeners?" — that is where the surprises live.
  • "React 17 or later?" — the delegation root moved in 17.

Code / implementation expected: Optional. Demonstrating the ordering is more convincing than describing it.

eventssynthetic eventsdom
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for React interviews — assumes DOM event bubbling. Difficulty: Medium

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. Every ordering claim below was executed against React 19.2.8 with real lis

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

Proving delegation: a native listener on the button can veto the React handler
Run Playground
import { useState, useRef, useEffect } from "react";

export default function App() {
  const [log, setLog] = useState([]);
  const [veto, setVeto] = useState(false);
  const btnRef = useRef(null);
  const say = (s) => setLog((l) => [...l, s]);

  // A REAL DOM listener on the button. If React had attached its handler to
  // this same node, stopping propagation here could not prevent it — the
  // handler would already have run. It does prevent it, which is the proof.
  useEffect(() => {
    const el = btnRef.current;
    if (!el) return;
    const onNative = (e) => {
      if (veto) e.stopPropagation();
    };
    el.addEventListener("click", onNative);
    return () => el.removeEventListener("click", onNative);
  }, [veto]);

  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.7, maxWidth: 560 }}>
      <label style={{ display: "block", marginBottom: 10, fontSize: 14 }}>
        <input type="checkbox" checked={veto} onChange={(e) => setVeto(e.target.checked)} />
        {" "}native listener on the button calls stopPropagation
      </label>

      {/* Capture and bubble handlers on both levels, from ONE delegated listener */}
      <div
        onClickCapture={() => say("1. parent onClickCapture")}
        onClick={() => say("4. parent onClick")}
        style={{ border: "1px dashed #aaa", borderRadius: 8, padding: 12 }}
      >
        <button
          ref={btnRef}
          onClickCapture={() => say("2. button onClickCapture")}
          onClick={() => say("3. button onClick")}
        >
          click me
        </button>
      </div>

      <button onClick={() => setLog([])} style={{ marginTop: 10 }}>clear</button>

      <pre style={{ background: "#f6f6f8", padding: 12, borderRadius: 8, fontSize: 12, marginTop: 12, minHeight: 110 }}>
{log.length ? log.join("\n") : "click the button"}
      </pre>

      <p style={{ fontSize: 13, color: "#666" }}>
        Unchecked: all four handlers fire in DOM order — capture downwards,
        bubble upwards — reconstructed from a single listener on the root
        container. Checked: nothing fires at all, because the event never
        reaches the container where React is listening.
      </p>
    </div>
  );
}
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 40 of 119 decoded in the React.js track. One more won't hurt.

Back to track