Lesson 43 of 80
The event object you get in onClick isn't the browser's — it's React's cross-browser wrapper around it, and it used to disappear from under you if you weren't careful.
11 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
When you write onClick={(e) => ...}, e isn't the raw browser event. It's a SyntheticEvent — an object React creates that wraps the native event and normalizes its interface across browsers, so e.target, e.preventDefault(), and friends behave identically in every environment React supports. This is the same instinct behind a lot of cross-browser tooling: paper over the differences once, in one place, instead of every call site checking which browser it's in.
Historically this came with a sharp edge: React pooled synthetic events for performance — it reused the same event object across renders and nulled out its fields right after your handler finished, so reading e.target inside a setTimeout or after an await got you null. React 17 removed event pooling entirely. Synthetic events are no longer reused or nulled — you can read one asynchronously without it going stale.
function SearchBox() {
async function handleChange(e) {
const value = e.target.value; // safe to read immediately
await logSearchIntent(value); // this used to matter — pooling would have nulled e by now
console.log("still safe:", e.target.value); // fine in React 17+
}
return <input onChange={handleChange} />;
}
Before React 17, that second console.log would have thrown or logged null, because by the time the await resumed, React had already recycled e for the next event. The fix people reached for was e.persist() — an escape hatch that told React "don't pool this one." e.persist() still exists but is now a no-op — there's nothing left to opt out of, since there's no pooling to opt out of.
If you ever need the actual, unwrapped browser event — say, to check a property React's synthetic wrapper doesn't expose — it's sitting right there on e.nativeEvent.
e.nativeEvent is an escape hatch, not the everyday API. Reach for it only when you need something the synthetic wrapper genuinely doesn't expose (browser-specific properties, certain low-level event details) — using it by default throws away the cross-browser consistency React is giving you.This is the same shape as JSX compiling down to createElement calls, or the virtual DOM sitting between your code and real DOM nodes — React interposes its own object between you and a messier underlying platform API, so your code targets React's consistent version instead of the browser's inconsistent one.