Join GitHub today
GitHub is home to over 31 million developers working together to host and review code, manage projects, and build software together.Sign up
DOM Event Mount Target Considerations #13713
Given that we’re considering a rewrite of the event system and are thinking about attaching events to the React root, I thought it would be fitting to explore all our options on where to mount the event listeners a little and combine all the knowledge that is scattered across the repo.
In general, there are three candidates for attaching event listeners:
Every option comes with shortcomings and I want to summarize what we’ve learned over the years.
Historically, React always listened at the document level and implemented a synthetic event system to simulate capture and bubble phases inside the React tree. Most event listeners are listening at the bubble phase which means that users can still add capture-level document listeners and see them fire before React will process the event.
Additionally, not all events bubble in the DOM. To support bubbling for all events, React sometimes eagerly adds event listeners (media events, for example) or listens to the capture phase instead.
While it usually works to leave the React event system and attach native listeners when needed, there are certain caveats that come with that. One example is that calling
Some browsers have certain optimizations in place that make handling of document listeners complicated (Safari is not properly bubbling when document listeners are used #12717, #12989 and Chrome doesn’t respect preventDefault() on touch start at the document level #11530 (comment)).
We’re also never cleaning up document-level listeners (#7128).
There are certain benefits of this solution as well. Our current event system can use the same "event bus" to implement polyfills that require document based listeners. Implementation of portal-bubbling is easier because we only need to ensure that we’re already listening at the document of the portal target (more on that later).
React Root Listeners (#2043)
One solution to the issues outlined above is the use of React roots as the mount target for events. This would still rely on event delegation and would require a synthetic event system.
Root level listeners would certainly help make bubbling more robust when using multiple React instances since we no longer add all listeners at the same level. This will, however, only work for bubbling. Some events use capturing (
Portal bubbling will also become more complicated since we have to find out if the portal root is inside the react-root to see if we need to attach listeners to the portal root as well. Consider the following example, where we need to add listeners to the
<body> <div id="react-root"></div> <div id="portal-root"></div> </body>
And compare it with this example, where we don’t need that:
<body> <div id="react-root"> <div id="portal-root"></div> </div> </body>
I’ve compiled a list of previous implementation attempts and the issues that were pointed out as well:
It’s probably possible to work around some/all of the issues. The invalid capturing order can be worked around by adding both a bubble and a capture listener for regular events and then only trigger the appropriate phase. The shims can probably be rewritten and if they need the document, additional listeners could be added. iOS tap highlight could be disabled via CSS. To get rid of some of the edge cases around events that don’t bubble in the browser, we should consider deprecating support for this all together.
I think we can (albeit with an additional implementation effort) support passive event systems while keep using event delegation: We’d add two different listeners (one for capturing and one for bubbling) and handle them as completely different events.
Support for shadow roots is a bit more complex as event delegation doesn’t really make sense there. We can’t easily consider the shadow root the same as a React root or a portal root since we can’t rely on adding listeners to the
<div id="react-root"> <!-- Listening on #react-root will catch events inside the #portal-root --> <div id="portal-root"></div> <!-- Listening on #react-root will NOT catch events inside the #shadow-root --> <my-component id="shadow-root"></my-component> </div>
Element Listeners (sort of #4751)
There’s a more radical approach to changing the event system and that is to get rid of event delegation altogether. This is what happens in Preact, react-dom-lite, Vue.js, and probably other frameworks as well (We should research Ember and Angular at this point).
In this case, it’s trivial to add support for passive event listeners and bubble/capture will behave as expected. It also allows us to completely remove the need of the synthetic event system as we could rely on the browser for this. We also know that some browsers can better optimize certain events if they are attached directly to the element (#1254, #1964).
I can think of at least two major roadblocks for this, though:
That’s all I have for now. I’m especially curious for ways how we could implement portal bubbling when using element listeners - I’d say this is the biggest uncertainty I have right now.
This is a really wonderful write-up!
This is a great consideration. The load/parse time of the code against the execution cost. I think it'll be important to create a good series of benchmark fixtures, though I worry about how much effort it will take to produce good ones. Does anyone know of existing projects they could be based on?
I'm not very familiar with async rendering, however I wonder if batching setState with event listeners remains useful in that mode (assuming medium/low priority setState calls are getting batched anyway). Is this a problem that could eventually go away?
Dispatching inside the same event listener seems straight forward. I'd like to know what event types often get batched together.
I am most excited about local event listener attachment. It seems like it avoids the most browser issues and that the challenges of making it work are within React. We have the most control over those problems.