Back to React
React
medium
mid

What are React synthetic events and how do they differ from native DOM events?

React wraps native DOM events in a cross-browser `SyntheticEvent` shim. Same API (`preventDefault`, `stopPropagation`, `target`) but normalized. React 17+ attaches listeners at the **root container**, not `document`; events bubble up through React's tree. Differences from native: no event pooling since React 17; `onChange` fires on every keystroke; `onScroll` doesn't bubble; some events use capture phase.

4 min read·~12 min to think through

Synthetic Events are React's cross-browser event wrapper. They look like native events but with normalized behavior across browsers and an attachment model tuned to React's tree.

What a SyntheticEvent is

A thin wrapper around the native event with the same API:

  • preventDefault()
  • stopPropagation()
  • target, currentTarget
  • type, bubbles, cancelable
  • nativeEvent for the underlying native event
jsx
<button onClick={(e) => {
  console.log(e.type);             // 'click'
  console.log(e.nativeEvent);      // underlying native MouseEvent
}}>Click</button>

Why React wraps events

Cross-browser normalization

Different browsers fire slightly different events with slightly different shapes. The wrapper smooths this over.

React-tree event flow

React attaches one delegated listener at the root container (in React 17+; was document before). Native events bubble to the root; React dispatches through the React component tree, which may differ from the DOM tree (portals).

This means React event handlers in a portal bubble up through the React parent even though the DOM portal node is elsewhere.

Performance via delegation

One listener at the root is cheaper than N per element. React handles delegation internally.

Differences from native events

1. Event pooling — gone in React 17+

Pre-17 React pooled SyntheticEvent objects for performance — the event you got was reused after the handler returned. Reading e.target later (e.g., in a setTimeout) gave null. You had to call e.persist().

In React 17+, pooling was removed. The event survives. e.persist() is a no-op.

2. Listener attached at root container, not document

Pre-17: document was the root listener target. 17+: the root container (<div id="root"> or similar).

This means multiple React trees on a page don't interfere with each other's events.

3. onChange fires on every keystroke

In native DOM, change on an <input> fires when the input loses focus. React's onChange is actually wired to native input — it fires on every keystroke. The native semantics are available via onChangeCapture and (more reliably) onInput.

4. onScroll doesn't bubble

Per the DOM spec, scroll doesn't bubble. React delegates events at the root, but scroll is special — handlers must be attached to the scrolling element itself.

5. Capture phase available

onClickCapture runs during capture phase (top-down) instead of bubble (bottom-up).

6. Some events not synthetic

window.onresize, native event listeners attached via addEventListener, etc. — those stay native.

Examples

Same as native

jsx
<button onClick={(e) => { e.preventDefault(); e.stopPropagation(); }}>...</button>

Portal bubbling through React tree

jsx
<Parent onClick={() => console.log("React parent")}>
  {createPortal(<Child />, document.body)}
</Parent>

// Click inside <Child>: 'React parent' logs (React bubble),
// even though DOM-wise the portal isn't a descendant.

Accessing the native event

jsx
<input onChange={(e) => {
  console.log(e.target.value);        // synthetic — works
  console.log(e.nativeEvent.data);    // native InputEvent property
}} />

Pitfalls

Async access (pre-17 only)

jsx
// Pre-17 — bug
onClick={(e) => setTimeout(() => console.log(e.target), 100);}   // null!
// Fix:
onClick={(e) => { e.persist(); setTimeout(() => console.log(e.target), 100); }}

Not a concern in 17+, but legacy codebases may still have e.persist() calls.

Mixing native and React listeners

If you attach a native listener via addEventListener AND a React onClick, they fire at different points in the bubble. Be deliberate.

stopPropagation vs stopImmediatePropagation

Synthetic stopPropagation stops React-tree bubbling but doesn't stop native bubbling at the root. Rarely matters; flag if you're mixing libraries.

Interview framing

"React wraps native DOM events in a cross-browser SyntheticEvent shim — same API (preventDefault, stopPropagation, target) but normalized. From React 17 onward, the listener is attached at the root container (not document), and event pooling was removed (so e.persist() is no longer needed). The biggest non-obvious behaviors: onChange fires on every keystroke (it's wired to native input), onScroll doesn't bubble (attach to the scrolling element), and events in portals bubble through the React tree — not the DOM tree — meaning a portal child's click reaches its React parent. Access the underlying event via e.nativeEvent when you need it."

Follow-up questions

  • Why did event pooling exist pre-17 and why was it removed?
  • How does an event in a portal bubble?
  • What's the difference between onChange and onInput?
  • Why doesn't onScroll bubble?

Common mistakes

  • Using onChange expecting native 'change' (blur) semantics.
  • Old e.persist() calls left over from pre-17.
  • Mixing addEventListener with React handlers without understanding order.
  • Expecting scroll events to bubble.

Performance considerations

  • Delegation at root is cheap. Avoid attaching N handlers when one delegated handler with `event.target` would do.

Edge cases

  • Synthetic events not fired for some native events (resize, scroll on non-element).
  • Custom events from Web Components.
  • Synthetic stopPropagation not affecting native handlers at root.

Real-world examples

  • React DnD, drag-and-drop libraries handle the native/synthetic boundary.
  • Portal-based modals/popovers.

Senior engineer discussion

Seniors understand the wrapper model, leverage portal-bubble-via-React-tree, distinguish synthetic from native event flow, and know which events aren't synthetic.

Related questions