Skip to the solution
MediumFrontend7 min read

How does React handle events?

960 views

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.

  • events
  • synthetic events
  • dom

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

7 min read · 9 sections

Try it in the playground

Open any example in the playground to run and change it.

ReactProving delegation: a native listener on the button can veto the React handler
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>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 40 of 119 in React.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All React.js questions
Up nextMedium

How do you handle asynchronous operations in React functional components?

Continue