Explain how React’s event system works (SyntheticEvent + root-level listeners) and why delegation is used: fewer listeners, easier updates for dynamic trees, consistent cross-browser behavior, and better integration with React’s scheduling/batching. Mention the React 17+ change (listeners attached to the root container instead of document) and common trade-offs (native vs synthetic, non-bubbling events).
Why does React use event delegation instead of native listeners?
Use guided tracks for structured prep, then practice company-specific question sets when you want targeted interview coverage.
Core idea
React usually doesn’t attach a native DOM listener to every element that has onClick/onChange, etc. Instead, it attaches a small set of listeners at the root container and uses event delegation: when the browser event bubbles up, React catches it once and dispatches it to the correct component handler via its internal tree.
Why delegation | What it buys React | What breaks with per-node native listeners |
|---|---|---|
Fewer listeners (memory + setup cost) | One listener can cover thousands of nodes | Big trees would create many listeners and slow mounts/updates |
Works with dynamic UIs | No attach/detach churn as elements appear/disappear during reconciliation | You’d constantly add/remove listeners on every commit |
Consistent behavior (SyntheticEvent) | Normalizes event quirks across browsers + consistent API | Different native event edge cases leak into app logic |
Tight integration with React scheduling | React can assign event priority (discrete/continuous) and coordinate updates | Harder to keep consistent ordering/priority if every node owns native listeners |
Portals + component tree semantics | Dispatch follows React’s tree (including across portals) rather than only DOM structure | Handlers become harder to reason about when UI spans multiple DOM subtrees |
How it works (mental model)
1) Browser fires a native event on a target node.
2) The event bubbles up to the root container.
3) React’s root listener runs once, finds the closest React component instance for the target, then runs the matching handler(s) in the right order (capture/bubble).
// Native DOM event delegation (concept)
const root = document.getElementById('root');
root.addEventListener('click', (e) => {
const btn = e.target.closest('button[data-id]');
if (!btn) return;
console.log('Clicked id:', btn.dataset.id);
});
// React does something similar internally:
// - one (or few) listeners at the root
// - map DOM target -> React fiber/component
// - call the matching onClick handler
React 17+ note
React attaches event listeners to the root container instead of the global document. This avoids interference when multiple React versions/roots coexist and keeps event handling scoped to a root.
Trade-off / gotcha | What you’ll notice | What to do |
|---|---|---|
Native vs React events |
| Use React handlers for UI updates; use native listeners in |
Non-bubbling events | Some events don’t bubble in the DOM (React may emulate via capture or special handling) | Know when to use capture props (e.g., |
Propagation expectations | React propagation follows React’s tree semantics (including portals), not only DOM nesting | When debugging, think in “React tree”, not just “DOM tree” |
React uses event delegation so it can handle events with a small number of root listeners, keep behavior consistent, avoid listener churn as the UI tree changes, and integrate event handling with React’s scheduling and update model. If it attached native listeners per element, large trees would pay heavy setup/teardown costs and React would lose control over consistent dispatch + prioritization.