What are Synthetic Events?
SyntheticEvents are React's cross-browser wrapper around native DOM events. They're like having a universal remote that works with any TV brand - same interface, different underlying systems.
Why Synthetic Events?
// Without React (Cross-browser nightmare)
function addEventListenerCrossBrowser(element, eventName, handler) {
if (element.addEventListener) {
// Modern browsers
element.addEventListener(eventName, handler, false);
} else if (element.attachEvent) {
// IE8 and below
element.attachEvent('on' + eventName, handler);
} else {
// Ancient browsers
element['on' + eventName] = handler;
}
}
// With React (Simple and consistent)
function ReactComponent() {
const handleClick = (e) => {
// e is a SyntheticEvent
console.log(e.type); // Works in all browsers
};
return <button onClick={handleClick}>Click me</button>;
}
SyntheticEvent Properties
function EventExplorer() {
const handleEvent = (e) => {
console.log('SyntheticEvent properties:');
console.log('type:', e.type);
console.log('target:', e.target);
console.log('currentTarget:', e.currentTarget);
console.log('eventPhase:', e.eventPhase);
console.log('bubbles:', e.bubbles);
console.log('cancelable:', e.cancelable);
console.log('timeStamp:', e.timeStamp);
console.log('defaultPrevented:', e.defaultPrevented);
console.log('isTrusted:', e.isTrusted);
// Access native event if needed
console.log('nativeEvent:', e.nativeEvent);
};
return (
<div>
<button onClick={handleEvent}>
Explore Event Properties
</button>
<input
onKeyDown={handleEvent}
placeholder="Type to see keyboard events"
/>
<div
onMouseMove={handleEvent}
style={{ height: '100px', backgroundColor: '#f0f0f0' }}
>
Move mouse here
</div>
</div>
);
}
Event Pooling (Legacy)
In React versions before 17, events were pooled for performance. This meant event objects were reused:
// React 16 and earlier - Event Pooling
function LegacyEventPooling() {
const handleClick = (e) => {
console.log(e.type); // Works
setTimeout(() => {
console.log(e.type); // null - event was recycled!
}, 1000);
// Had to persist event for async operations
e.persist();
setTimeout(() => {
console.log(e.type); // Now works
}, 1000);
};
return <button onClick={handleClick}>Click me</button>;
}
// React 17+ - No Event Pooling
function ModernEventHandling() {
const handleClick = (e) => {
console.log(e.type); // Works
setTimeout(() => {
console.log(e.type); // Still works!
}, 1000);
};
return <button onClick={handleClick}>Click me</button>;
}
Event Delegation in React
React uses event delegation to optimize performance. Instead of attaching listeners to each element, React attaches a single listener at the document root:
// How React handles events internally (simplified)
class ReactEventSystem {
constructor() {
// Single listener for all events
document.addEventListener('click', this.handleEvent, true);
document.addEventListener('keydown', this.handleEvent, true);
// ... other event types
}
handleEvent = (nativeEvent) => {
// Find React component that should handle this event
const targetComponent = this.findReactComponent(nativeEvent.target);
// Create SyntheticEvent
const syntheticEvent = this.createSyntheticEvent(nativeEvent);
// Call component's event handler
if (targetComponent && targetComponent.props.onClick) {
targetComponent.props.onClick(syntheticEvent);
}
}
createSyntheticEvent(nativeEvent) {
return {
type: nativeEvent.type,
target: nativeEvent.target,
currentTarget: nativeEvent.currentTarget,
preventDefault: () => nativeEvent.preventDefault(),
stopPropagation: () => nativeEvent.stopPropagation(),
nativeEvent: nativeEvent,
// ... other properties
};
}
}
Types of Synthetic Events
1. Mouse Events
function MouseEventDemo() {
const handleMouseEvent = (e) => {
// All mouse events are SyntheticMouseEvent instances
console.log(`Mouse event: ${e.type}`);
console.log(`Button: ${e.button}`);
console.log(`Coordinates: (${e.clientX}, ${e.clientY})`);
console.log(`Alt key: ${e.altKey}`);
console.log(`Ctrl key: ${e.ctrlKey}`);
console.log(`Shift key: ${e.shiftKey}`);
};
return (
<div
onClick={handleMouseEvent}
onDoubleClick={handleMouseEvent}
onMouseDown={handleMouseEvent}
onMouseUp={handleMouseEvent}
onMouseEnter={handleMouseEvent}
onMouseLeave={handleMouseEvent}
onMouseMove={handleMouseEvent}
onMouseOut={handleMouseEvent}
onMouseOver={handleMouseEvent}
onContextMenu={handleMouseEvent}
style={{ padding: '20px', backgroundColor: '#f5f5f5' }}
>
Try different mouse interactions
</div>
);
}
2. Keyboard Events
function KeyboardEventDemo() {
const handleKeyboardEvent = (e) => {
// All keyboard events are SyntheticKeyboardEvent instances
console.log(`Keyboard event: ${e.type}`);
console.log(`Key: ${e.key}`);
console.log(`Code: ${e.code}`);
console.log(`KeyCode: ${e.keyCode}`);
console.log(`Alt key: ${e.altKey}`);
console.log(`Ctrl key: ${e.ctrlKey}`);
console.log(`Shift key: ${e.shiftKey}`);
console.log(`Meta key: ${e.metaKey}`);
console.log(`Repeat: ${e.repeat}`);
};
return (
<input
onKeyDown={handleKeyboardEvent}
onKeyUp={handleKeyboardEvent}
onKeyPress={handleKeyboardEvent} // Deprecated but still supported
placeholder="Type here to see keyboard events"
/>
);
}
3. Focus Events
function FocusEventDemo() {
const handleFocusEvent = (e) => {
// All focus events are SyntheticFocusEvent instances
console.log(`Focus event: ${e.type}`);
console.log(`Related target: ${e.relatedTarget}`);
};
return (
<div>
<input
onFocus={handleFocusEvent}
onBlur={handleFocusEvent}
placeholder="Focus me"
/>
<button
onFocus={handleFocusEvent}
onBlur={handleFocusEvent}
>
Tab to me
</button>
</div>
);
}
4. Form Events
function FormEventDemo() {
const handleFormEvent = (e) => {
// Form events include change, input, submit
console.log(`Form event: ${e.type}`);
if (e.type === 'change' || e.type === 'input') {
console.log(`Value: ${e.target.value}`);
}
if (e.type === 'submit') {
e.preventDefault();
console.log('Form submitted');
}
};
return (
<form onSubmit={handleFormEvent}>
<input
onChange={handleFormEvent}
onInput={handleFormEvent}
placeholder="Type here"
/>
<select onChange={handleFormEvent}>
<option value="">Select...</option>
<option value="1">Option 1</option>
<option value="2">Option 2</option>
</select>
<button type="submit">Submit</button>
</form>
);
}
Cross-Browser Normalization
// React normalizes browser differences automatically
function CrossBrowserExample() {
const handleWheel = (e) => {
// React normalizes wheel events across browsers
console.log('Delta Y:', e.deltaY);
console.log('Delta Mode:', e.deltaMode);
};
const handleInput = (e) => {
// Input events work consistently across browsers
console.log('Input value:', e.target.value);
};
const handleKeyDown = (e) => {
// Key values are normalized
if (e.key === 'Enter') {
console.log('Enter pressed');
}
// No need to check e.keyCode, e.which, etc.
};
return (
<div>
<div
onWheel={handleWheel}
style={{ height: '100px', overflow: 'auto' }}
>
Scroll me (wheel events normalized)
</div>
<input
onInput={handleInput}
placeholder="Type (input events normalized)"
/>
<input
onKeyDown={handleKeyDown}
placeholder="Press Enter (key events normalized)"
/>
</div>
);
}
SyntheticEvent Performance
Event Handler Performance
function PerformanceExample() {
// Inefficient: Creates new function on each render
const inefficientRender = () => {
return items.map(item => (
<button
key={item.id}
onClick={() => handleClick(item.id)}
>
{item.name}
</button>
));
};
// Efficient: Stable function reference
const handleClick = useCallback((id) => {
console.log('Clicked:', id);
}, []);
const efficientRender = () => {
return items.map(item => (
<MemoizedButton
key={item.id}
id={item.id}
name={item.name}
onClick={handleClick}
/>
));
};
return <div>{efficientRender()}</div>;
}
const MemoizedButton = React.memo(({ id, name, onClick }) => {
return (
<button onClick={() => onClick(id)}>
{name}
</button>
);
});
Custom Synthetic Events
// Creating custom event-like behavior
function useCustomEvent(eventName) {
const [listeners, setListeners] = useState([]);
const addEventListener = useCallback((listener) => {
setListeners(prev => [...prev, listener]);
}, []);
const removeEventListener = useCallback((listener) => {
setListeners(prev => prev.filter(l => l !== listener));
}, []);
const dispatchEvent = useCallback((data) => {
const event = {
type: eventName,
data,
timeStamp: Date.now(),
preventDefault: () => {},
stopPropagation: () => {}
};
listeners.forEach(listener => listener(event));
}, [listeners, eventName]);
return { addEventListener, removeEventListener, dispatchEvent };
}
// Usage
function CustomEventComponent() {
const { addEventListener, dispatchEvent } = useCustomEvent('customAction');
useEffect(() => {
const handler = (e) => {
console.log('Custom event received:', e.data);
};
addEventListener(handler);
return () => removeEventListener(handler);
}, [addEventListener]);
return (
<button onClick={() => dispatchEvent({ message: 'Hello!' })}>
Trigger Custom Event
</button>
);
}
Working with Native Events
function NativeEventAccess() {
const handleClick = (e) => {
// Access native event when needed
const nativeEvent = e.nativeEvent;
console.log('SyntheticEvent type:', e.type);
console.log('Native event type:', nativeEvent.type);
// Some properties only available on native event
console.log('Native timestamp:', nativeEvent.timeStamp);
console.log('Native path:', nativeEvent.path);
};
// Sometimes you need to work with native events directly
const elementRef = useRef(null);
useEffect(() => {
const element = elementRef.current;
if (!element) return;
// Native event listener for specific cases
const handleNativeEvent = (e) => {
console.log('Native event handler:', e);
};
element.addEventListener('customEvent', handleNativeEvent);
return () => {
element.removeEventListener('customEvent', handleNativeEvent);
};
}, []);
return (
<div ref={elementRef} onClick={handleClick}>
Click to compare synthetic and native events
</div>
);
}
Event System Gotchas
1. Event.target vs Event.currentTarget
function TargetVsCurrentTarget() {
const handleClick = (e) => {
console.log('target:', e.target);
console.log('currentTarget:', e.currentTarget);
// target: element that triggered the event
// currentTarget: element with the event listener
};
return (
<div onClick={handleClick} style={{ padding: '20px', border: '1px solid' }}>
<button>Click me</button>
<span>Or click me</span>
</div>
);
}
2. Async Event Access
function AsyncEventAccess() {
const handleClick = async (e) => {
// In React 17+, this works fine
console.log(e.type); // 'click'
await new Promise(resolve => setTimeout(resolve, 1000));
console.log(e.type); // Still 'click'
// But be careful with mutable properties
console.log(e.target.value); // Might have changed
};
const safeAsyncAccess = async (e) => {
// Store values you need before async operations
const value = e.target.value;
const type = e.type;
await someAsyncOperation();
// Use stored values
console.log(type, value);
};
return (
<input
onClick={handleClick}
onChange={safeAsyncAccess}
defaultValue="Click or type"
/>
);
}
Debugging Synthetic Events
function EventDebugger() {
const [events, setEvents] = useState([]);
const logEvent = (e) => {
const eventInfo = {
type: e.type,
target: e.target.tagName,
timeStamp: e.timeStamp,
bubbles: e.bubbles,
cancelable: e.cancelable,
eventPhase: e.eventPhase,
isTrusted: e.isTrusted
};
setEvents(prev => [...prev.slice(-9), eventInfo]);
};
return (
<div>
<div
onClick={logEvent}
onMouseMove={logEvent}
onKeyDown={logEvent}
style={{ padding: '20px', border: '1px solid' }}
>
<input
onChange={logEvent}
onFocus={logEvent}
onBlur={logEvent}
placeholder="Interact to log events"
/>
<button onClick={logEvent}>Click me</button>
</div>
<div style={{ marginTop: '20px' }}>
<h3>Recent Events:</h3>
<pre>{JSON.stringify(events, null, 2)}</pre>
</div>
</div>
);
}