When does React create SyntheticEvents?
One of the reasons is performance. Rather than attaching event listeners to every event you use inside the app, React attaches a single event listener for every event type to document
. Then, when you create a, for example, onClick
event on a div
, it just adds that to it's internal event listener map. Once click on a div happens, React's listener on document node finds your event listener and calls it with SyntheticEvent.
This way, react doesn't have to re-create and clear listeners from DOM all the time, it just changes it's internal listener registry.
The other reason is that React's event bubbling works differently compared to DOM one in some cases. Portals in particular.
Take this for example:
const MyComponent = () => (
<div onClick={() => console.log('I was clicked!')}>
MyComponent
<SomeModalThatUsesPortalComponent>A modal</SomeModalThatUsesPortalComponent>
</div>
);
const SomeModalThatUsesPortalComponent = () => {
return ReactDOM.createPortal(
<div onClick={() => console.log('Modal clicked!')}>this.props.children</div>,
document.getElementById('myModalsPortal')
);
}
This would end up with following DOM:
<body>
<div>
My Component
</div>
<div id="myModalsPortal">
<div>A modal</div>
</div>
</body>
So in this case when using a Portal, DOM structure doesn't match the component structure exactly. Here is where bubbling behaviour diverges:
- Clicking on
A modal
will not trigger a native click event onMyComponent's
div - It will trigger React's
onClick
handler onMyComponent's
div, because modal is a child component ofMyComponent
There is no mention in the React source of the reason for waiting for events to bubble in order to dispatch SyntheticEvent
s (that I can find).
The decision to wait for events that bubble to finish their bubbling before trapping them in the React events system is apparently only a matter of code organization.
Reading React's ReactBrowserEventEmitter listenTo
function, simplified here to make it more easily understandable:
for (let i = 0; i < dependencies.length; i++) {
const dependency = dependencies[i];
switch (dependency) {
case: // All event types that do NOT bubble
trapCapturedEvent(dependency, mountAt);
default:
// By default, listen on the top level to all non-media events.
// Media events don't bubble so adding the listener wouldn't do anything.
const isMediaEvent = mediaEventTypes.indexOf(dependency) !== -1;
if (!isMediaEvent) {
trapBubbledEvent(dependency, mountAt);
}
break;
}
isListening[dependency] = true;
}
The easiest way to separate events that bubble from those that don't (and keep code readable) seems to be to trap events that bubble in their bubbling phase, and those that don't in the capture phase.
So, in summary, the actual way events work (to make an analogy with my initial proposition in the question) is the following:
For events that bubble:
- The native event capture phase happens
- The native event reaches target
- The native event bubbles back up
- React catches it at the document layer with (an abstracted version of)
addEventListener(type, handler, false) // False stands for "catch it in the bubbling phase"
- React puts it in its
ReactBrowserEventEmitter
- React simulates another full capture/bubble roundtrip for the SyntheticEvent
- React runs the handlers you built in your code
For events that don't bubble:
- The native event capture phase happens
- React catches it at the layer in which you set the listener in your code with (an abstracted version of)
addEventListener(type, handler, true) // True stands for "catch it in the capture phase"
- React puts it in its
ReactBrowserEventEmitter
- React simulates another full capture/bubble roundtrip for the SyntheticEvent
- React runs the handlers you built in your code