Synthetic Events
An event handler is a prop in camelCase that takes a function:
onClick={handleClick}. It receives a SyntheticEvent: a cross-browser
wrapper with the interface of the native event (target, preventDefault(),
stopPropagation()); the native event is in e.nativeEvent. React does not
attach a listener to every element: it uses event delegation, one listener
per event type on the root container (since React 17; before that, on
document). Events propagate through the React tree, so an event inside a
portal reaches its React parents.
Applies to: React 19.3.
Details
Handlers
- Pass a function, don't call it:
onClick={handleClick}, notonClick={handleClick()}. With an argument:onClick={() => remove(id)}. return falsedoes not prevent the default action: calle.preventDefault().- Capture phase: add
Captureto the name,onClickCapture. e.currentTargetis the element whose handler is running,e.targetis where the event happened.- Handlers are the place for side effects caused by the user: they don't run during render, see You Might Not Need an Effect.
Differences from DOM Events
| React | DOM |
|---|---|
onChange fires on every input, like input | change fires when the value is committed (text fields: on blur) |
onFocus, onBlur bubble (focusin, focusout under the hood) | focus, blur don't bubble |
onScroll does not bubble (React 16 bubbled it) | scroll does not bubble |
onMouseEnter, onMouseLeave don't bubble | The same |
| Propagates through the React tree (portals) | Propagates through the DOM tree |
- No event pooling since React 17: the event object can be read in a timeout
or after
await;e.persist()does nothing. onTouchStart,onTouchMoveandonWheelare passive:e.preventDefault()in them is ignored. To block scrolling, add a listener with{ passive: false }in an effect.
Delegation
createRootattaches listeners for all supported events to the root container (in both phases for events that bubble, in the capture phase for the rest). When an event reaches the root, React finds the fiber of the target and calls the handlers along the React tree.- Why the root and not
document(React 17): several React apps and versions on one page don't interfere, ande.stopPropagation()in React really stops the event before it reachesdocument. - Mixing with
addEventListener. A native listener on an element runs before the React handlers in the bubble phase, because React handles the event only when it reaches the root:e.stopPropagation()in a React handler stops other React handlers and native listeners ondocumentandwindow, but not native listeners inside the root;e.stopPropagation()in a native listener inside the root stops the event before the root, so the React handlers in the bubble phase don't run.
- Subscribe to events of
windowanddocument(resize,keydownon the whole page) withaddEventListenerin an effect, with cleanup.
Example
The order of native and React handlers:
import { useEffect, useRef } from 'react';
export default function App() {
const buttonRef = useRef(null);
useEffect(() => {
const button = buttonRef.current;
const onButtonClick = () => console.log('native: button');
const onDocumentClick = () => console.log('native: document');
button.addEventListener('click', onButtonClick);
document.addEventListener('click', onDocumentClick);
return () => {
button.removeEventListener('click', onButtonClick);
document.removeEventListener('click', onDocumentClick);
};
}, []);
return (
<div onClick={() => console.log('react: div')}>
<button
ref={buttonRef}
onClick={(e) => {
console.log('react: button');
e.stopPropagation();
}}
>
Click
</button>
</div>
);
}
// Click:
// native: button
// react: button
// "react: div" and "native: document" are not logged
Follow-up Questions
- What is a synthetic event? A React wrapper over the native event with the same interface and consistent behavior across browsers.
- How to get the native event?
e.nativeEvent. - Does a click inside a portal reach the
onClickof a React parent? Yes: events follow the React tree, even though the portal's DOM is elsewhere. - Why did React 17 move delegation from
documentto the root? To allow several React versions on a page and to makestopPropagationwork with listeners ondocument. - Why does
e.preventDefault()inonWheelnot block scrolling? The listener is passive.
Common Mistakes
onClick={handleClick()}: the function is called during render.return falseto prevent the default action.- Expecting
onChangeto fire on blur, as in the DOM. - "React attaches a listener to every element."
- Expecting
e.stopPropagation()in a React handler to stop a native listener on a parent element inside the app.