React Synthetic Events: Native vs Abstraction
React synthetic events are wrapper objects around native browser events. Instead of attaching listeners directly to DOM elements, React uses event delegation—it attaches a single listener to the root of the document and determines which component should handle the event. A synthetic event implements the W3C Events spec for consistency across browsers, so your code works identically in Chrome, Firefox, Safari, and Edge without special handling. The key difference from native events is that synthetic events don't exist after the callback finishes; in React 17+, they're pooled and reused to reduce garbage collection overhead.
Why Does React Wrap Native Events in SyntheticEvent?
React wraps native browser events for three reasons: cross-browser consistency, performance through event pooling, and efficient event handling via delegation. Native browser events have quirky behavior differences across browsers. For example, the target property on change events isn't always what you'd expect, and event-stopping behavior differs subtly. React's synthetic events normalize these differences, so you write code once and it works everywhere. Additionally, React uses a single listener on the root document rather than attaching listeners to each element, which saves memory and improves performance on large component trees. This is called event delegation.
Event Delegation and Synthetic Events
React attaches event listeners to the root of the React tree, not to individual elements. When you click a button deep in your component tree, the click bubbles up to the root, and React's event system determines which component should handle it. This is more efficient than creating thousands of individual listeners.
// Example: React attaches a single 'click' listener at the root
// When you click any button, it bubbles to the root and React routes it
export const EventDelegationExample = () => {
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
console.log("Button clicked, event is synthetic");
console.log(event.nativeEvent); // Access the underlying native event
};
return (
<div>
<button onClick={handleClick}>Click me</button>
<button onClick={handleClick}>Or me</button>
<button onClick={handleClick}>Or me</button>
</div>
);
};
The event.nativeEvent property gives you access to the underlying native browser event if you need it. However, most React code never needs this; the synthetic event is sufficient. Event delegation is transparent to the developer, but understanding it explains why React's event system is so fast.
Synthetic Event Properties Are Safe and Consistent
The synthetic event object has properties like currentTarget, target, type, preventDefault(), and stopPropagation(). These are standardized across all event types and browsers. In TypeScript, accessing these properties is safe because the type system ensures they exist.
// Synthetic events provide consistent, type-safe properties
export const SyntheticEventDemo = () => {
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
// These properties are guaranteed to exist and be the correct type
const inputValue = event.currentTarget.value;
const inputName = event.currentTarget.name;
const eventType = event.type; // "change"
console.log(`${inputName} changed to ${inputValue}`);
};
return (
<input
type="text"
name="email"
onChange={handleChange}
placeholder="Enter email"
/>
);
};
When you access event.currentTarget.value, TypeScript knows currentTarget is an HTMLInputElement (because of the generic parameter), so it knows value is a string property. This prevents typos and makes refactoring safer.
Event Pooling in React 17+
In React 16 and earlier, synthetic events were pooled (reused) to reduce memory allocation. After the event handler finishes, the event object was reset and returned to the pool. In React 17+, pooling was removed, so events persist indefinitely after the handler. This means if you were using the event in an async handler, it would work in React 17+ but fail in React 16. For new code targeting React 17+, you don't need to worry about this.
// React 17+ doesn't require event persistence; the event outlives the handler
export const AsyncEventHandler = () => {
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.currentTarget.value; // Safe to access, even after await
// In React 16, you'd need event.persist() here
await new Promise(resolve => setTimeout(resolve, 1000));
console.log("Value after 1 second:", value); // Works in React 17+
};
return <input type="text" onChange={handleChange} />;
};
Stopping Event Propagation with Type Safety
Synthetic events have stopPropagation() and preventDefault() methods. These are typed correctly in TypeScript, so you can call them without errors.
// Type-safe event propagation methods
export const StopPropagationDemo = () => {
const handleButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation(); // Prevents the click from bubbling to parent
console.log("Button clicked, event stopped");
};
const handleDivClick = (event: React.MouseEvent<HTMLDivElement>) => {
console.log("Div clicked"); // Won't log if button was clicked
};
return (
<div onClick={handleDivClick}>
<button onClick={handleButtonClick}>Click inside div</button>
</div>
);
};
When you call event.stopPropagation(), the event doesn't bubble further up the component tree. This is useful for modal dialogs or dropdowns that should close when clicked.
Key Takeaways
- React synthetic events are normalized wrappers around native browser events, ensuring consistent behavior across all browsers.
- React uses event delegation: a single listener at the root handles all events, routing them to the appropriate components.
- In React 17+, event pooling is removed, so events persist after handlers finish, making them safe for async operations.
- Synthetic events provide consistent properties and methods like
currentTarget,preventDefault(), andstopPropagation(). - TypeScript ensures you can't access invalid properties on synthetic events, preventing bugs.
Frequently Asked Questions
When should I access the native event via nativeEvent?
Rarely. The synthetic event covers 99% of use cases. Use nativeEvent only when you need low-level browser APIs not exposed by React, such as getModifierState() for keyboard modifier keys or accessing the actual event phase.
Does React's event delegation work for all event types?
React's event delegation covers most common events (click, change, focus, blur, scroll, etc.), but some events like scroll and resize can't be delegated in the normal way, so they're attached directly to the element. In practice, this doesn't affect your code—you still type them the same way.
How do I prevent the default behavior of a form submission?
Call event.preventDefault() in your onSubmit handler. This stops the browser from reloading the page and sending the form data to the server the traditional way.
Can I attach event listeners to DOM elements directly if I use React?
You can, but it's not recommended. If you attach a native listener with element.addEventListener(), it won't interact with React's event system, and you lose the benefits of delegation and type safety. Use React's event props (like onClick) instead.
Are synthetic events slower than native events?
No, the indirection is negligible. Event delegation is actually faster than attaching thousands of individual listeners, and modern JavaScript engines optimize away the synthetic event abstraction.