Topics in this subject
React 3 min read Updated 5 Aug 2026

Events

Synthetic events, camelCase handlers, passing arguments, preventDefault/stopPropagation, and root-level delegation.

Synthetic events#

React wraps native DOM events in a SyntheticEvent — a cross-browser normalized object with the same API (e.target, e.preventDefault(), etc.). Handlers are passed in camelCase as JSX props, and you pass a function reference, not a call.

<button onClick={handleClick}>Save</button>     {/* 🟢 reference */}
<button onClick={handleClick()}>Save</button>    {/* ⚠️ calls on render */}
<input onChange={e => setName(e.target.value)} />

Passing arguments#

Wrap in an inline arrow so the extra args are captured without invoking early.

{items.map(item => (
  <li key={item.id} onClick={() => remove(item.id)}>{item.label}</li>
))}

⚠️ In React 16 you had to call e.persist() to use an event asynchronously (events were pooled). Pooling was removed in React 17 — the synthetic event is no longer reused, so e.persist() is a no-op and rarely needed. 🎯

preventDefault & stopPropagation#

function Form() {
  function onSubmit(e) {
    e.preventDefault();      // stop full-page reload
    // ...submit via fetch
  }
  return (
    <form onSubmit={onSubmit}>
      <button onClick={e => e.stopPropagation()}>Inner</button> {/* halt bubbling */}
    </form>
  );
}
Method Effect
e.preventDefault() Cancel the browser default (navigation, form submit, checkbox toggle)
e.stopPropagation() Stop the event bubbling to parent handlers
e.target The element that fired the event
e.currentTarget The element the handler is attached to

Delegation under the hood 🎯#

React does not attach a listener to every element. It attaches one listener per event type at the root container (the DOM node you passed to createRoot) and dispatches synthetic events by walking the fiber tree. Before React 17 this root was document; since React 17 it is the root container, which lets multiple React versions coexist on one page.

flowchart LR
  A["Native event fires on child"] --> B["Bubbles to root container"]
  B --> C["React's single root listener"]
  C --> D["Build SyntheticEvent"]
  D --> E["Dispatch down fiber tree to your onClick"]

Because delegation is centralized, inline arrow handlers create a tiny closure per render but do not add DOM listeners — so they are usually fine. Only memoize handlers (useCallback) when passing them to memoized children or when profiling shows a real cost. 🟢

Interview Q&A#

Q1. What is a SyntheticEvent? A cross-browser wrapper around the native DOM event exposing a consistent API. It normalizes differences and integrates with React's dispatch system.

Q2. Where does React attach event listeners? One listener per event type on the root container (the createRoot node). React 17 moved this off document to enable multiple React roots/versions on one page.

Q3. Is e.persist() still needed? No. Event pooling was removed in React 17, so synthetic events survive after the handler returns; e.persist() is now a no-op.

Q4. Are inline arrow handlers a performance problem? Rarely. They allocate a small closure per render but add no DOM listeners thanks to delegation. Only optimize with useCallback when passing to memoized children.

Q5. e.target vs e.currentTarget? target is the element that originated the event; currentTarget is the element whose handler is currently running (useful with delegation/bubbling).