skip to content

In React 19, what object does React pass to an event handler such as onClick, and how do you reach the underlying browser event from it?

level: juniorimportance: must knowfreq 65%

answer

  1. not the raw browser event
  2. same interface everywhere
  3. React normalizes onChange and onFocus
  4. an escape hatch hangs off the event
  5. e.nativeEvent

basics

~10 s

React passes a SyntheticEvent: a cross-browser wrapper that mirrors the DOM event interface (type, target, preventDefault, stopPropagation). The untouched browser event is always available on e.nativeEvent for anything React does not normalize.

solid answer

~40 s

Handlers receive a **SyntheticEvent**, not the raw DOM event. It mirrors the W3C event interface — `type`, `target`, `currentTarget`, `bubbles`, `eventPhase`, `isTrusted`, `preventDefault()`, `stopPropagation()` — with the same shape and behaviour in every browser, plus React's own `isDefaultPrevented()` and `isPropagationStopped()`. React also smooths over inconsistent native events: `onChange` on a text input fires on every keystroke because it is backed by the native `input` event, and `onFocus`/`onBlur` bubble because React uses `focusin`/`focusout`. Anything React does not normalize is still reachable: every synthetic event carries the original browser object on `e.nativeEvent`. Since React 17 the wrapper is no longer pooled, so you can store it, close over it, and read it after an `await` without calling `e.persist()`.

code

javascript · 10 lines
javascript
function Card() {
  function handleClick(e) {
    console.log(e.type, e.target, e.currentTarget);
    console.log(e.nativeEvent instanceof MouseEvent); // true
    e.preventDefault();
    console.log(e.isDefaultPrevented()); // true
  }

  return <button onClick={handleClick}>Pick</button>;
}

go deeper

for a junior

Be able to say the handler receives a SyntheticEvent that behaves like a DOM event, and name e.nativeEvent as the way to the real browser event.

for a middle

Explain what normalization actually buys you — onChange backed by the native input event, onFocus/onBlur bubbling via focusin/focusout — and the extra predicates isDefaultPrevented() and isPropagationStopped().

for a senior

Show judgment about the escape hatch: reading e.nativeEvent means taking on raw browser behaviour and its support matrix, so know when that trade is worth making and how you would verify it.

for a principal

Frame the wrapper as an API-stability decision: a normalized surface lets React change its dispatch implementation without breaking app code, and it is why removing pooling in React 17 was a non-breaking change for most codebases.

## What React actually hands you When you write `<button onClick={handleClick}>`, React does not hand `handleClick` the object the browser created. It hands you a **SyntheticEvent** — a React-created wrapper object that holds a reference to the browser's event and re-exposes its interface. ```js function handleClick(e) { console.log(e.type); // 'click' console.log(e.target); // the DOM node that was clicked console.log(e.currentTarget); // the node this handler is attached to in the React tree console.log(e.nativeEvent); // the browser's MouseEvent } ``` The wrapper is not a stripped-down object. It implements the same members the W3C event model defines: `bubbles`, `cancelable`, `currentTarget`, `defaultPrevented`, `eventPhase`, `isTrusted`, `target`, `timeStamp`, `type`, plus `preventDefault()` and `stopPropagation()`. Type-specific events add their own normalized properties — a mouse event carries `clientX`, `clientY`, `button`, `shiftKey`, `metaKey`; a keyboard event carries `key`, `code`, `repeat`, `getModifierState()`. ## Two things React adds Beyond the DOM surface, React adds `isDefaultPrevented()` and `isPropagationStopped()`. These are React's own predicates: a downstream handler can ask whether an earlier handler already cancelled the event, which is useful when several components cooperate on the same interaction. ## Why a wrapper at all Historically the answer was purely cross-browser normalization — old browsers disagreed about property names, about whether an event bubbled, and about how you cancelled it. Modern browsers agree on most of that, but the wrapper still earns its place for two reasons. First, React *synthesizes* events that do not map one-to-one onto native ones. React's `onChange` on a text input fires on every keystroke; the native `change` event fires only when the field is committed. React implements `onChange` on top of the native `input` event so that controlled inputs have a single sensible hook. Likewise `onFocus` and `onBlur` bubble in React, because React backs them with the bubbling `focusin`/`focusout` events rather than the non-bubbling `focus`/`blur`. Second, a uniform object gives React a place to hang its own semantics — the predicates above, and a consistent notion of `currentTarget` expressed in terms of the *React* tree rather than raw DOM ancestry. ## The escape hatch: e.nativeEvent Normalization means React exposes a curated subset. When you need something outside that subset, `e.nativeEvent` gives you the real browser object, untouched: ```js function handleSubmit(e) { e.preventDefault(); const button = e.nativeEvent.submitter; // which button submitted the form } ``` This is a supported, documented property — not a private field. Reaching for it is normal; you just accept that whatever you read there is browser behaviour, not React behaviour, so it is on you to check support. ## What is no longer true Many older answers add "but the event is pooled, so copy what you need before it is recycled." That was accurate up to React 16. React 17 removed pooling entirely, and React 19 keeps it removed. The synthetic event is an ordinary object with an ordinary lifetime: you can stash it in a variable, put it in a closure, or read it after awaiting a request. `e.persist()` is still callable so old code does not break, but it does nothing. One caveat survives, and it is a DOM rule rather than a React rule: `e.currentTarget` is only meaningful while the event is being dispatched. Once dispatch finishes, the browser resets it, so reading `e.currentTarget` after an `await` gives `null` — for native events too. Read it synchronously and keep the value if you need it later. ## What to say in an interview Name the object, say it mirrors the DOM event interface with cross-browser normalization, name `e.nativeEvent` as the escape hatch, and mention that pooling is gone. That covers everything an interviewer is checking for.

  • Name a case where React's synthetic event behaves differently from the native event of the same name.
    `onChange` is the clearest one. The native `change` event on a text input fires only when the value is committed (blur or Enter), while React's `onChange` fires on every keystroke because React implements it on the native `input` event. `onFocus` and `onBlur` are another: React makes them bubble by using `focusin`/`focusout` underneath, whereas native `focus`/`blur` do not bubble.
  • What is the difference between e.target and e.currentTarget in a React handler?
    `e.target` is the node where the interaction actually happened — often a deep descendant, such as the `<span>` inside a button. `e.currentTarget` is the node whose handler is running, i.e. the element you put the `onClick` on. Use `currentTarget` when you want the component's own element, and read it synchronously: it is reset once dispatch ends.
  • Does e.nativeEvent give you a different object for each handler that sees the event?
    No. The native event is created once by the browser for that interaction, and every synthetic wrapper React creates while dispatching it points at that same object. So mutating or reading state on `e.nativeEvent` in one handler is visible to the others, unlike the React-level predicates such as `isPropagationStopped()`.

saying these in an interview costs you the question

  • Says React handlers receive the raw DOM event
  • Claims nativeEvent is private React internals you must not touch
  • Insists you must call e.persist() before async use
  • Thinks SyntheticEvent lacks preventDefault or stopPropagation
  • Says React's onChange is just the native change event

context