Suggest an editImprove this articleRefine the answer for “Synthetic events in React”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**SyntheticEvent** is React's cross-browser wrapper around native DOM events. It normalizes properties like `event.target.value` across browsers and registers a single root-level listener instead of one per element. ```jsx function handleClick(e) { e.preventDefault(); // same in every browser console.log(e.type); // "click" console.log(e.nativeEvent); // raw DOM event if needed } ``` **Key point:** React 17 removed event pooling, so async access to event properties works without calling `event.persist()`.Shown above the full answer for quick recall.Answer (EN)Image**SyntheticEvent** is React's cross-browser wrapper around native DOM events. It normalizes properties like `event.target.value` so they behave the same in Chrome, Firefox, and older IE, and uses a single root-level listener instead of one per element. ## Theory ### TL;DR - Native events in different browsers have slightly different properties. SyntheticEvent gives you one consistent API on top. - React attaches ONE listener to `#root`, not one per element. That is event delegation. - In React 16, event objects were pooled and reused after the handler finished. Async access to `event.target` returned `null`. React 17 removed this behavior. - `e.target` is the element that triggered the event. `e.currentTarget` is the element where the handler lives. - Access the raw DOM event via `e.nativeEvent` when a third-party library needs it. ### Quick example ```javascript function Button() { const handleClick = (e) => { console.log(e.type); // "click" - normalized console.log(e.nativeEvent); // MouseEvent - the raw DOM event e.preventDefault(); // works the same in every browser }; return <a href="/somewhere" onClick={handleClick}>Click</a>; } // Prevents navigation, logs "click" with no browser differences ``` The `e` React passes to your handler is not a native `MouseEvent`. It wraps one. Same shape, same methods, plus `e.nativeEvent` to reach the original. ### How event delegation works React does not attach a listener to each button, input, or link you render. It registers one listener on the root DOM node (`#root` in React 17+, `document` in React 16). Native events bubble up as usual. React intercepts at the root, wraps the native event in a SyntheticEvent object, normalizes properties, and dispatches to the matching handler. A list of 1000 buttons still has exactly one event listener in the browser. Dynamic lists handle clicks without re-binding. The change from `document` to `#root` in React 17 matters when mixing React versions on one page. Each React tree now has its own isolated listener. ### Event pooling: React 16 vs React 17 In React 16, SyntheticEvent objects were pooled. After your handler returned, React cleared all properties on the object and put it back in the pool for reuse. This broke async code in a non-obvious way: ```javascript // React 16 - breaks function handleClick(e) { setTimeout(() => { console.log(e.target); // null - object was recycled }, 0); } ``` The fix was `e.persist()`, which pulled the event out of the pool: ```javascript // React 16 - fixed function handleClick(e) { e.persist(); setTimeout(() => { console.log(e.target); // <button> }, 0); } ``` React 17 removed pooling entirely. Events are regular objects now. `persist()` still exists as a no-op for backward compatibility. I have seen the async-null bug in real production logs enough times that I now always capture event values synchronously, regardless of React version. The safer pattern works in both: ```javascript function handleClick(e) { const value = e.target.value; // capture synchronously setTimeout(() => console.log(value), 0); // always works } ``` ### event.target vs event.currentTarget Say you have a button with a span inside: ```javascript <button onClick={handle}> <span>Click me</span> </button> ``` The user clicks the `<span>`. Now `e.target` is `<span>` and `e.currentTarget` is `<button>`. The handler is on the button. The click landed on the span. For forms, read from `e.currentTarget` when you want the element that has the handler, not the element that received the click. ### Common mistakes **Reading event properties in async code without capturing them first.** ```javascript // Broke in React 16, works in React 17+ but still unclear intent const handleChange = (e) => { setTimeout(() => setState(e.target.value), 0); }; // Explicit and safe everywhere const handleChange = (e) => { const val = e.target.value; setTimeout(() => setState(val), 0); }; ``` **Using `addEventListener` inside `useEffect` for React-rendered elements.** ```javascript // Avoid - bypasses React's delegation, can fire twice, leaks memory useEffect(() => { document.getElementById('btn').addEventListener('click', handler); }); // Do this instead <button onClick={handler}>Click</button> ``` Native `addEventListener` in `useEffect` is fine for window-level events like `resize` or `scroll` that have no React prop equivalent. Always return a cleanup function. **Returning `false` to prevent the default action**, like in plain HTML. In React, `return false` from an event handler does nothing. Call `e.preventDefault()` explicitly. **Confusing `e.target` and `e.currentTarget` in form handlers.** When a form field contains nested elements, `e.target` may point to a child span. Always use `e.currentTarget` when you want the element with the handler. ### Real-world usage - React components - every `onClick`, `onChange`, `onSubmit` you write receives a SyntheticEvent by default - Next.js - form handlers in the app router work the same way transparently - Material UI - SyntheticEvents pass through from its button and input components to your handlers unchanged - D3.js integration - use `e.nativeEvent` when D3 expects a raw DOM event object - TypeScript - import `MouseEvent`, `ChangeEvent`, `FormEvent` from `react`, not from DOM type definitions ### Follow-up questions **Q:** What is the difference between `event.target` and `event.currentTarget`? **A:** `target` is the element that received the event, meaning the deepest element clicked. `currentTarget` is the element where the handler is attached. They differ when the clicked element is a child of the handler element. **Q:** Why did React 17 move event listeners from `document` to `#root`? **A:** Attaching to `document` caused conflicts when running two React versions on one page or mixing React with other frameworks. Attaching to the root node isolates each React tree so they do not interfere with each other. **Q:** Is `event.persist()` still needed in React 17+? **A:** No. Pooling was removed in React 17, so `persist()` is a no-op. You will see it in older codebases - safe to remove, harmless to leave. **Q:** How does React simulate `stopPropagation()` inside its own dispatch system? **A:** Calling `e.stopPropagation()` sets an internal flag in React's dispatch loop. React stops calling parent handlers. The native event still bubbles up to the root where React's single listener sits, but React does not dispatch it further up the component tree. **Q (senior):** How do SyntheticEvents behave with portals? **A:** Events from portal elements bubble through the React component tree, not the DOM tree. A click inside a modal portal will trigger handlers on the React parent even if the modal's DOM node is mounted outside that subtree. Use `e.nativeEvent` if you need the actual DOM propagation path. ## Examples ### Basic: preventing default navigation ```javascript function NavLink() { const handleClick = (e) => { e.preventDefault(); // stops browser navigation console.log(e.type); // "click" console.log(e.currentTarget.href); // the link's href attribute }; return <a href="/dashboard" onClick={handleClick}>Dashboard</a>; } // Logs "click" and the href - no page reload happens ``` Note that `e.currentTarget` here is the `<a>` tag, regardless of what child element was clicked inside it. ### Intermediate: async form submission ```javascript function PaymentForm({ onSubmit }) { const [card, setCard] = useState(''); const handleChange = (e) => { setCard(e.target.value); // read synchronously - safe in all React versions }; const handleSubmit = async (e) => { e.preventDefault(); // card comes from state, not from the event object // so there is no async pooling issue const res = await fetch('/charge', { method: 'POST', body: JSON.stringify({ card }), }); onSubmit(await res.json()); }; return ( <form onSubmit={handleSubmit}> <input value={card} onChange={handleChange} placeholder="Card number" /> <button type="submit">Pay</button> </form> ); } // handleChange reads event.target.value synchronously and stores in state. // The async handler never touches the event object directly. ``` ### Advanced: event capture phase ```javascript function CaptureDemo() { return ( <div onClickCapture={() => console.log('1. div capture')} onClick={() => console.log('4. div bubble')} > <button onClickCapture={() => console.log('2. button capture')} onClick={() => console.log('3. button bubble')} > Click </button> </div> ); } // Output when button is clicked: // 1. div capture // 2. button capture // 3. button bubble // 4. div bubble // // React supports capture phase via the *Capture suffix on any event prop. // Useful for intercepting events before they reach any child handler. ``` Capture handlers run top-down before the element receives the event. This lets a parent component intercept and cancel events before any child has a chance to handle them.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.