React 14 ⚛️ Synthetic Events Object
When you write an event handler in React, the object you receive is not the browser’s native event. It is a SyntheticEvent, a wrapper that React creates around the native event. The wrapper exposes the same interface — target, preventDefault(), stopPropagation(), type — but it is a plain JavaScript object that React controls. Understanding the synthetic event object explains why React event handlers behave consistently across browsers, why the object can be used after the handler returns in modern React, and why the old advice about event.persist() is obsolete.
The synthetic event is part of React’s effort to provide a stable, predictable API regardless of which browser the application runs in. Instead of writing conditional code for old versions of Internet Explorer and modern browsers, you write one handler that uses one API. React normalizes the differences and dispatches the same wrapper everywhere. This is one of the reasons React became popular for cross-browser applications, and it remains a core part of how React events work.
This chapter covers what the synthetic event is, how React creates and dispatches it, the properties and methods it exposes, the difference between target and currentTarget, the removal of event pooling, the nativeEvent escape hatch, and the patterns that depend on understanding the synthetic event object.
Key point: React wraps native browser events in SyntheticEvent objects. The synthetic event has the same interface as the native event (target, currentTarget, preventDefault(), stopPropagation(), type) and normalizes behavior across browsers. Since React 17, synthetic events are not pooled, so they can be used asynchronously. Use event.nativeEvent to access the original browser event.
Why synthetic events exist
The cross-browser problem. Before modern browsers converged on standards, event handling differed across browsers. The event object had different property names, this referred to different things, and preventDefault and return false behaved differently. React’s synthetic event normalizes all of this into a single API, so the same handler works everywhere.
The delegation problem. React does not attach a listener to every element. It attaches a single listener at the root of the React tree and dispatches events to the appropriate components. When an event fires, React walks the React component tree to find the handlers that should run, creates synthetic events for them, and calls them in order. This is more efficient than per-element listeners, and it is why React can update handlers without touching the DOM.
The consistency problem. The synthetic event presents a consistent interface regardless of how the native event was produced. A click from a mouse, a trackpad, or a touchscreen produces the same synthetic MouseEvent with the same properties. This simplifies event handling code.
The pooling problem (historical). In React 16 and earlier, synthetic events were pooled. React created a small number of event objects and reused them across handlers to reduce garbage collection. After the handler returned, the event object’s properties were nulled out, and the object was returned to the pool. This meant the event object could not be accessed asynchronously — a reference held in a setTimeout or a promise would see an empty object. React provided event.persist() to remove the event from the pool, allowing asynchronous access.
The pooling removal (React 17+). React 17 removed event pooling. Synthetic events are now created per dispatch and are not reused. The event object remains valid after the handler returns, and event.persist() is no longer necessary. The method still exists but does nothing.
a. What the synthetic event is
A synthetic event is a JavaScript object with the same properties and methods as the native event, plus a reference to the native event.
function handleClick(event) {
console.log(event.type); // "click"
console.log(event.target); // the element that fired
console.log(event.currentTarget); // the element with the handler
console.log(event.nativeEvent); // the original browser event
}
React creates the synthetic event when the native event fires and dispatches it to the appropriate handler. The synthetic event’s interface is defined by React and is consistent across browsers.
The properties and methods are grouped into categories.
| Category | Properties and methods |
|---|---|
| Identification | type, timeStamp, isTrusted |
| Target | target, currentTarget |
| Propagation | stopPropagation(), stopImmediatePropagation(), isPropagationStopped() |
| Default | preventDefault(), isDefaultPrevented() |
| Native | nativeEvent |
| Event-specific | key, code, value, clientX, clientY, and others |
The event-specific properties depend on the event type. A KeyboardEvent has key and code; a MouseEvent has clientX and clientY; a ChangeEvent has target.value.
b. target versus currentTarget
The two most commonly confused properties are target and currentTarget.
event.target is the element that dispatched the event — the deepest element that was clicked. event.currentTarget is the element whose handler is currently running — the element with the onClick attribute.
function handleClick(event) {
console.log('target:', event.target.tagName); // the clicked element
console.log('currentTarget:', event.currentTarget.tagName); // the handler's element
}
return (
<div onClick={handleClick}>
<span>Click me</span>
</div>
);
If the user clicks the <span>, event.target is the span and event.currentTarget is the div. The handler is attached to the div, so currentTarget is the div. The event bubbled from the span, so target is the span.
During bubbling, currentTarget changes as the event moves up the tree, but React sets it to the element whose handler is running. target remains constant.
A common pattern is to read the value from target in a change handler:
const handleChange = (event) => {
const value = event.target.value; // the input that changed
setValue(value);
};
Another common pattern is to check whether the click was directly on the element or on a child, using currentTarget:
const handleClick = (event) => {
if (event.target === event.currentTarget) {
// clicked directly on the element, not a child
}
};
This is used for modals that close when the backdrop is clicked but not when the content is clicked.
c. preventDefault and stopPropagation
The two methods that control event behavior are preventDefault() and stopPropagation().
event.preventDefault() stops the browser’s default behavior for the event. For a form submission, it stops the page reload. For a link click, it stops navigation. For a checkbox, it stops the toggle.
const handleSubmit = (event) => {
event.preventDefault();
// handle the submission manually
};
event.stopPropagation() stops the event from bubbling to ancestor handlers. For a nested click handler, it prevents the parent’s handler from firing.
const handleInnerClick = (event) => {
event.stopPropagation();
// the outer handler will not fire
};
React also provides isDefaultPrevented() and isPropagationStopped() to check whether the methods have been called. These are rarely used in application code but are useful in libraries that need to inspect event state.
stopImmediatePropagation() stops the event from reaching other handlers on the same element, in addition to stopping bubbling. React supports it for parity with the native API.
d. The nativeEvent escape hatch
The synthetic event exposes a subset of the native event’s properties. When a property is not exposed, event.nativeEvent provides the original browser event.
const handleClick = (event) => {
console.log(event.nativeEvent); // the original MouseEvent
console.log(event.nativeEvent.offsetX); // a property not on SyntheticEvent
};
The native event is a real browser event object with all the properties the browser provides. It is useful when the synthetic event’s interface does not cover a specific property, or when integrating with a library that expects a native event.
Using nativeEvent couples the handler to the browser’s event API, which reduces the cross-browser guarantee that the synthetic event provides. It should be used only when necessary.
e. Event pooling removal
In React 16 and earlier, the synthetic event was pooled, and the object could not be accessed after the handler returned.
// React 16 behavior: broken
const handleClick = (event) => {
setTimeout(() => {
console.log(event.type); // null — the event was pooled
}, 0);
};
// React 16 fix: call persist
const handleClick = (event) => {
event.persist();
setTimeout(() => {
console.log(event.type); // "click"
}, 0);
};
React 17 removed pooling. The same code now works without persist():
// React 17+: works
const handleClick = (event) => {
setTimeout(() => {
console.log(event.type); // "click"
}, 0);
};
The persist() method still exists for backward compatibility, but it does nothing. Code that calls it continues to work, but the call can be removed.
The removal of pooling means that synthetic events behave like normal JavaScript objects: they can be stored, passed to async functions, and accessed after the synchronous handler returns. This simplifies patterns that involve debouncing, throttling, or passing the event to a service.
Complete Example Session
// ============================================
// PART 1: BASIC SYNTHETIC EVENT
// ============================================
function Button() {
const handleClick = (event) => {
console.log(event.type); // "click"
console.log(event.target); // the button element
console.log(event.currentTarget); // the button element
};
return <button onClick={handleClick}>Click</button>;
}
// ============================================
// PART 2: TARGET VS CURRENTTARGET
// ============================================
function Card() {
const handleClick = (event) => {
console.log('target:', event.target.tagName);
console.log('currentTarget:', event.currentTarget.tagName);
};
return (
<div onClick={handleClick}>
<span>Click me</span>
</div>
);
// Clicking the span: target=SPAN, currentTarget=DIV
}
// ============================================
// PART 3: PREVENTDEFAULT ON FORM
// ============================================
function LoginForm() {
const handleSubmit = (event) => {
event.preventDefault();
console.log('form submitted');
};
return (
<form onSubmit={handleSubmit}>
<button type="submit">Log in</button>
</form>
);
}
// ============================================
// PART 4: STOPPROPAGATION
// ============================================
function Nested() {
const handleOuter = () => console.log('outer');
const handleInner = (event) => {
event.stopPropagation();
console.log('inner');
};
return (
<div onClick={handleOuter}>
<button onClick={handleInner}>Inner</button>
</div>
);
// Clicking inner logs "inner" only
}
// ============================================
// PART 5: READING INPUT VALUE
// ============================================
function Search() {
const [query, setQuery] = useState('');
const handleChange = (event) => {
setQuery(event.target.value);
};
return <input value={query} onChange={handleChange} />;
}
// ============================================
// PART 6: KEYBOARD EVENT
// ============================================
function Input() {
const handleKeyDown = (event) => {
console.log('key:', event.key); // "Enter", "Escape", "a"
console.log('code:', event.code); // "Enter", "Escape", "KeyA"
};
return <input onKeyDown={handleKeyDown} />;
}
// ============================================
// PART 7: MOUSE EVENT COORDINATES
// ============================================
function TrackMouse() {
const handleClick = (event) => {
console.log('clientX:', event.clientX);
console.log('clientY:', event.clientY);
};
return <div onClick={handleClick}>Click</div>;
}
// ============================================
// PART 8: NATIVE EVENT ACCESS
// ============================================
function NativeAccess() {
const handleClick = (event) => {
console.log(event.nativeEvent.offsetX); // not on SyntheticEvent
};
return <div onClick={handleClick}>Click</div>;
}
// ============================================
// PART 9: ASYNC EVENT ACCESS (REACT 17+)
// ============================================
function AsyncEvent() {
const handleClick = (event) => {
setTimeout(() => {
console.log(event.type); // works in React 17+
}, 1000);
};
return <button onClick={handleClick}>Click</button>;
}
// ============================================
// PART 10: CHECKING TARGET VS CURRENTTARGET
// ============================================
function Modal({ onClose, children }) {
const handleBackdropClick = (event) => {
if (event.target === event.currentTarget) {
onClose();
}
};
return (
<div className="backdrop" onClick={handleBackdropClick}>
<div className="modal-content">{children}</div>
</div>
);
}
These ten parts cover the basic event object, target versus currentTarget, preventDefault, stopPropagation, reading input values, keyboard events, mouse coordinates, nativeEvent, async event access in React 17+, and the backdrop-click pattern.
Quick Reference
SyntheticEvent Properties
| Property | Purpose |
|---|---|
type | Event type: “click”, “change” |
target | Element that dispatched the event |
currentTarget | Element with the handler |
nativeEvent | Original browser event |
timeStamp | When the event occurred |
isTrusted | Whether the event was user-generated |
defaultPrevented | Whether preventDefault was called |
SyntheticEvent Methods
| Method | Purpose |
|---|---|
preventDefault() | Stop browser default behavior |
stopPropagation() | Stop event bubbling |
stopImmediatePropagation() | Stop bubbling and same-element handlers |
isDefaultPrevented() | Check if default was prevented |
isPropagationStopped() | Check if propagation was stopped |
persist() | No-op in React 17+ |
Event-Specific Properties
| Event type | Properties |
|---|---|
| MouseEvent | clientX, clientY, button, altKey |
| KeyboardEvent | key, code, altKey, ctrlKey |
| ChangeEvent | target.value, target.checked |
| FormEvent | target.elements |
| FocusEvent | target, relatedTarget |
| TouchEvent | touches, changedTouches |
target vs currentTarget
| Property | Meaning | Changes during bubbling |
|---|---|---|
target | Element that fired the event | No |
currentTarget | Element with the running handler | Yes |
Best Practices
✅ Do This:
// Read input value from target
const handleChange = (e) => setValue(e.target.value);
// Prevent default on forms
const handleSubmit = (e) => { e.preventDefault(); ... };
// Stop propagation in nested handlers
const handleInner = (e) => { e.stopPropagation(); ... };
// Compare target to currentTarget for backdrop clicks
if (event.target === event.currentTarget) onClose();
// Use nativeEvent only when necessary
const offset = event.nativeEvent.offsetX;
❌ Don’t Do This:
// Call persist (no-op in React 17+)
event.persist(); // ❌ unnecessary
// Mutate the event
event.target = something; // ❌
// Rely on pooling behavior
// (React 16 only — no longer relevant)
// Use nativeEvent for everything
event.nativeEvent.target.value // ❌ use event.target.value
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
target is a child element | Event bubbled | Use currentTarget for the handler’s element |
currentTarget is null in async | Old React pooling | Not an issue in React 17+ |
preventDefault not working | Called on wrong event | Verify the event type |
stopPropagation blocks parent | Intentional but surprising | Document the behavior |
nativeEvent undefined | Used in a test with synthetic event | Provide a native event in tests |
| Value not updating | Read from wrong target | Check target.value |
Real-World Examples
1. Controlled Input
const handleChange = (e) => setValue(e.target.value);
2. Form Submit
const handleSubmit = (e) => { e.preventDefault(); save(); };
3. Backdrop Click
if (event.target === event.currentTarget) onClose();
4. Nested Click
const handleInner = (e) => { e.stopPropagation(); doInner(); };
5. Keyboard Shortcut
const handleKeyDown = (e) => {
if (e.key === 'Enter') submit();
};
6. Mouse Coordinates
const handleClick = (e) => console.log(e.clientX, e.clientY);
7. Async Event Access
const handleClick = (e) => {
setTimeout(() => console.log(e.type), 1000);
};
8. Native Event Property
console.log(event.nativeEvent.offsetX);
9. Check Default Prevented
if (event.isDefaultPrevented()) { ... }
10. Event Type Check
if (event.type === 'click') { ... }
Visual
Synthetic Event Flow
┌──────────────────────────────────────────────────────────────┐
│ User clicks button │
│ │ │
│ ▼ │
│ Browser fires native event │
│ │ │
│ ▼ │
│ React root listener receives event │
│ │ │
│ ▼ │
│ React walks component tree to find handlers │
│ │ │
│ ▼ │
│ React creates SyntheticEvent wrapping native event │
│ │ │
│ ▼ │
│ Handler called with SyntheticEvent │
│ │ │
│ ▼ │
│ event.target → clicked element │
│ event.currentTarget → element with handler │
│ event.nativeEvent → original browser event │
└──────────────────────────────────────────────────────────────┘
target vs currentTarget
┌──────────────────────────────────────────────────────────────┐
│ <div onClick={handleClick}> ← currentTarget │
│ <span>Click me</span> ← target (if clicked) │
│ </div> │
│ │
│ Click on span: │
│ target = <span> │
│ currentTarget = <div> │
│ │
│ Click on div (outside span): │
│ target = <div> │
│ currentTarget = <div> │
│ │
│ target is always the deepest element clicked. │
│ currentTarget is always the element with the handler. │
└──────────────────────────────────────────────────────────────┘
Event Pooling History
┌──────────────────────────────────────────────────────────────┐
│ REACT 16: │
│ Event object pooled │
│ Properties nulled after handler returns │
│ Async access requires event.persist() │
│ │
│ REACT 17+: │
│ Event object created per dispatch │
│ Properties remain valid │
│ event.persist() is a no-op │
│ Async access works without special handling │
└──────────────────────────────────────────────────────────────┘
Backdrop Click Pattern
┌──────────────────────────────────────────────────────────────┐
│ <div onClick={handleBackdropClick}> ← backdrop │
│ <div className="modal-content"> │
│ <p>Modal content</p> │
│ </div> │
│ </div> │
│ │
│ Click on backdrop: │
│ target = backdrop div │
│ currentTarget = backdrop div │
│ target === currentTarget → close modal │
│ │
│ Click on content: │
│ target = content div │
│ currentTarget = backdrop div │
│ target !== currentTarget → do not close │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| SyntheticEvent | React wrapper around native event |
| Interface | Same as native event |
target | Element that dispatched the event |
currentTarget | Element with the handler |
preventDefault() | Stop default browser behavior |
stopPropagation() | Stop event bubbling |
nativeEvent | Original browser event |
| Event pooling | Removed in React 17 |
persist() | No-op in React 17+ |
| Event types | MouseEvent, KeyboardEvent, ChangeEvent, etc. |
| Async access | Works in React 17+ without persist |
Key takeaways:
- React wraps native events in
SyntheticEventobjects. The wrapper has the same interface as the native event and normalizes behavior across browsers, so the same handler works everywhere. targetis the element that fired the event;currentTargetis the element with the handler. During bubbling,targetstays constant andcurrentTargetis the element whose handler is running.preventDefault()stops browser defaults;stopPropagation()stops bubbling. These are the two methods that control event behavior, and they are the same as on native events.- Event pooling was removed in React 17. Synthetic events are now created per dispatch and remain valid after the handler returns.
event.persist()is a no-op and can be removed. event.nativeEventprovides the original browser event. It is the escape hatch when the synthetic event does not expose a property the native event has.- Event-specific properties are available on the synthetic event.
event.target.valuefor change events,event.keyfor keyboard events,event.clientXfor mouse events. - The backdrop click pattern uses
target === currentTarget. Comparing the two distinguishes a click on the element itself from a click on a child, which is how modals close only when the backdrop is clicked.
Remember: The synthetic event object is the interface between browser events and React handlers. It looks like a native event because it presents the same properties and methods, but it is a React-created wrapper that normalizes behavior and provides a consistent API. The two properties that cause the most confusion are target and currentTarget, and the difference is what they represent: the element that fired the event versus the element whose handler is running. The two methods that control behavior are preventDefault() and stopPropagation(), and they work the same way as on native events. Since React 17, the event object is not pooled, so it can be used asynchronously and persist() is unnecessary. When the synthetic event does not have what you need, nativeEvent provides the original. Understanding these details makes event handling predictable, and the patterns that depend on them — controlled inputs, form submissions, nested clicks, backdrop closing — become obvious.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!