Synthetic Events in React
What are Synthetic Events?
Synthetic Events are React's wrapper around native browser events. React creates a unified event system that works the same across all browsers.
function Button() {
function handleClick(event) {
console.log(event); // SyntheticEvent, not native Event
}
return <button onClick={handleClick}>Click me</button>;
}
Why are they needed?
Cross-browser compatibility
Different browsers have different event implementations. Synthetic Events unify the API:
function Input() {
function handleChange(event) {
// event.target.value works the same everywhere
console.log(event.target.value);
}
return <input onChange={handleChange} />;
}
Performance
React uses event delegation: all handlers are attached to the root element, not to each element separately.
// Instead of 1000 handlers on each button
// React creates one handler at the root
function List({ items }) {
return (
<ul>
{items.map(item => (
<li key={item.id}>
<button onClick={() => console.log(item.id)}>
{item.name}
</button>
</li>
))}
</ul>
);
}
Synthetic Event API
Synthetic Event has the same interface as native events:
function Form() {
function handleSubmit(event) {
event.preventDefault(); // Works like native event
event.stopPropagation();
console.log(event.type); // 'submit'
console.log(event.target); // element reference
console.log(event.currentTarget); // element with handler
}
return (
<form onSubmit={handleSubmit}>
<button type="submit">Submit</button>
</form>
);
}
Event Pooling (before React 17)
In React 16 and earlier, events were reused for performance:
// React 16 and earlier
function handleClick(event) {
console.log(event.type); // 'click'
setTimeout(() => {
console.log(event.type); // null, event cleared!
}, 0);
}
To preserve an event, you used event.persist():
function handleClick(event) {
event.persist(); // Preserve event
setTimeout(() => {
console.log(event.type); // 'click', works!
}, 0);
}
React 17+:
In React 17, event pooling was removed. Events are no longer cleared, event.persist() is not needed.
Accessing native event
You can get the native event through nativeEvent:
function handleClick(event) {
console.log(event); // SyntheticEvent
console.log(event.nativeEvent); // Native browser Event
// Useful for browser-specific features
console.log(event.nativeEvent.path);
}
Differences from native events
Naming
React uses camelCase instead of lowercase:
// HTML
<button onclick="handleClick()">Click</button>
// React
<button onClick={handleClick}>Click</button>
Returning false
In HTML you can return false to prevent default action:
<!-- HTML -->
<a href="#" onclick="console.log('clicked'); return false">
Link
</a>
In React you must explicitly call preventDefault():
// React
function Link() {
function handleClick(event) {
event.preventDefault();
console.log('clicked');
}
return <a href="#" onClick={handleClick}>Link</a>;
}
Supported events
React supports all standard DOM events:
Mouse events
onClick
onContextMenu
onDoubleClick
onDrag
onDragEnd
onDragEnter
onDragExit
onDragLeave
onDragOver
onDragStart
onDrop
onMouseDown
onMouseEnter
onMouseLeave
onMouseMove
onMouseOut
onMouseOver
onMouseUp
Keyboard events
function Input() {
function handleKeyDown(event) {
if (event.key === 'Enter') {
console.log('Enter pressed');
}
}
return <input onKeyDown={handleKeyDown} />;
}
Form events
function Form() {
return (
<form
onSubmit={e => e.preventDefault()}
onChange={e => console.log('changed')}
onFocus={e => console.log('focused')}
onBlur={e => console.log('blurred')}
>
<input type="text" />
</form>
);
}
Focus events
onFocus
onBlur
Touch events
onTouchStart
onTouchMove
onTouchEnd
onTouchCancel
Event delegation
React 16 and earlier
Events were attached to document:
// React attached handlers to document
document.addEventListener('click', handleAllClicks);
React 17+
Events are attached to the React root node:
// React attaches to app root
const root = document.getElementById('root');
root.addEventListener('click', handleAllClicks);
This is important when using multiple React versions on one page or integrating with other libraries.
Event handling specifics
onChange vs onInput
In React onChange works like native onInput — fires on every change:
function Input() {
const [value, setValue] = useState('');
return (
<input
value={value}
// Fires on every keystroke
onChange={e => setValue(e.target.value)}
/>
);
}
onScroll
onScroll doesn't bubble in React (as in DOM), but React provides it for convenience:
function ScrollableDiv() {
function handleScroll(event) {
console.log(event.target.scrollTop);
}
return (
<div onScroll={handleScroll} style={{ height: 200, overflow: 'auto' }}>
{/* Lots of content */}
</div>
);
}
Direct native handler attachment
Sometimes you need to attach a native handler directly:
function Component() {
const ref = useRef(null);
useEffect(() => {
const element = ref.current;
function handleNativeClick(event) {
console.log('Native click', event);
}
element.addEventListener('click', handleNativeClick);
return () => {
element.removeEventListener('click', handleNativeClick);
};
}, []);
return <div ref={ref}>Click me</div>;
}
Warning:
Be careful with direct handlers — don't forget to remove them in cleanup function.
Event capturing
React supports the capture phase:
function Parent() {
return (
<div
onClickCapture={() => console.log('1. Parent capture')}
onClick={() => console.log('3. Parent bubble')}
>
<button
onClickCapture={() => console.log('2. Button capture')}
onClick={() => console.log('4. Button bubble')}
>
Click
</button>
</div>
);
}
// Clicking button outputs:
// 1. Parent capture
// 2. Button capture
// 4. Button bubble
// 3. Parent bubble
TypeScript typing
import { MouseEvent, ChangeEvent, FormEvent } from 'react';
function Component() {
function handleClick(event: MouseEvent<HTMLButtonElement>) {
console.log(event.currentTarget.name);
}
function handleChange(event: ChangeEvent<HTMLInputElement>) {
console.log(event.target.value);
}
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
}
return (
<form onSubmit={handleSubmit}>
<input onChange={handleChange} />
<button onClick={handleClick}>Submit</button>
</form>
);
}
Common mistakes
Asynchronous event usage (React 16)
// Wrong in React 16
function handleClick(event) {
setTimeout(() => {
console.log(event.type); // null!
}, 0);
}
// Correct
function handleClick(event) {
const eventType = event.type; // Save value
setTimeout(() => {
console.log(eventType); // 'click'
}, 0);
}
Passing event to setState
// Wrong
function handleChange(event) {
setState(prevState => ({
...prevState,
value: event.target.value // event might be cleared
}));
}
// Correct
function handleChange(event) {
const newValue = event.target.value;
setState(prevState => ({
...prevState,
value: newValue
}));
}
Conclusion
Synthetic Events:
- Wrapper around native browser events
- Provide cross-browser compatibility
- Use delegation for performance
- Have the same API as native events
- In React 17+ are not cleared automatically
- Use camelCase for naming
- Native event accessible through
nativeEvent
In interviews:
Important to be able to:
- Explain what Synthetic Events are and why they're needed
- Describe event delegation
- Explain differences from native events
- Describe changes in React 17 (event pooling)
- Show how to access native event