SyntheticEvent Root Delegation vs Native DOM Dispatch
- Last updated
- Prerequisites:
- DOM Event bubbling and capturing
- EventTarget.addEventListener
- react
- synthetic-event
- event-delegation
- dom
- bubbling
- performance
Read at your depth
The practical view
React wraps browser native events in a cross-browser `SyntheticEvent` wrapper. In React 17+, React no longer attaches event listeners to the global `document` node; instead, it delegates and attaches all event listeners to the root DOM container (e.g. `<div id="root">`) where `createRoot()` was mounted. Calling `e.stopPropagation()` stops bubbling within the React virtual tree.
The same idea in other frameworks
Root container event delegation (`container.addEventListener`).Zone.js patched element-level `addEventListener` binding directly on DOM nodes.Direct native element event listeners attached during VNode patching.Svelte 5 uses centralized document/root event delegation for efficiency.Legacy vs modern
Direct DOM Listener vs React SyntheticEvent Delegation
Direct DOM listeners attach per-node causing memory bloat; React delegates listeners to the root container.
elements.forEach(el => el.addEventListener('click', handler)); // ❌ Thousands of listener allocations<li onClick={() => handleClick(item.id)}>{item.name}</li> // ✅ NEW: Delegated to #rootInterview gotchas
Context
Interviewers test your knowledge of React's multi-version coexistence and architecture evolution.
The mechanical answer
In React 16, all synthetic events were attached to `document`. This broke multi-version micro-frontends and nested non-React libraries because calling `e.stopPropagation()` in an inner tree didn't prevent outer `document` listeners from firing. In React 17+, event listeners are attached to the **root DOM container** (the element passed to `createRoot`), allowing multiple independent React roots to safely coexist on the same page.
Trap
Do not say that React attaches event listeners directly to individual DOM elements like `<button>`. Everything is delegated to the root container.
Context
Checks knowledge of the removal of Event Pooling.
The mechanical answer
No. Event Pooling was completely removed in React 17. In React 18 and 19, `SyntheticEvent` objects are not recycled into a shared pool, so you can freely read `e.target` inside an asynchronous `setTimeout` or `await` without calling `e.persist()`.
Trap
In older React versions (<17), reading `e.target` in an async callback without `e.persist()` resulted in `null` values due to object pooling.