Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Attach Listeners Eagerly to Roots and Portal Containers #19659

Merged
merged 4 commits into from
Aug 24, 2020

Conversation

gaearon
Copy link
Collaborator

@gaearon gaearon commented Aug 20, 2020

Fixes #19608.

It uncovered a flaw in our current bubbling strategy: if the portal tree doesn't have a particular listener (e.g. onClick), but the parent tree does, the event doesn't bubble (because we don't listen to that even on the portal container node).

This fix is to listen to all supported events at the time the root or the portal container are attached. This means we need to know all of them ahead of time, but we already do.

I added a new flag called enableEagerRootListeners that turns this change on (set to false everywhere except WWW where it's dynamic). All of the existing behavior is behind its falsy branch since we'd like a safety net. I edited all existing codesites that are branch-specific to be wrapped so they're easy to find and delete later, whichever way we decide to go.

Aside from fixing #19608, this change also has a few other benefits:

  • It surfaces less common conditions more reliably (e.g. it uncovered Fix movementX/Y polyfill with capture events #19672 and that event replaying has regressed for capture events)
  • It aligns the behavior closer between modes when Event Replaying is on and off
  • It makes issues like http://github.com/facebook/react/issues/3639 impossible.
  • While it adds some extra work for each root and portal container, it only happens once, but it removes all the CPU work (including Map accesses) we're currently doing for every event listener on every render.

The downside of this change is that it makes createEventHandle less powerful, as it would only work for known events. However, the list of known events is finite, and we can manually add the missing ones if there is a need. Behind the flag, I've made it throw for unknown events.

This also introduces some risk in case browsers do something unfortunate when a particular event is registered. (However, this would have already been the case for when you eventually add an event handler somewhere deep.) At least with the Passive flag on the bad ones should be mitigated.

Review without whitespace: https://github.com/facebook/react/pull/19659/files?w=1

@facebook-github-bot facebook-github-bot added CLA Signed React Core Team Opened by a member of the React Core Team labels Aug 20, 2020
@codesandbox-ci
Copy link

codesandbox-ci bot commented Aug 20, 2020

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 46ee6be:

Sandbox Source
React Configuration
great-bell-nptin Issue #19608

@sizebot
Copy link

sizebot commented Aug 20, 2020

Details of bundled changes.

Comparing: 90d212d...46ee6be

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom.development.js +0.1% +0.1% 881.27 KB 881.99 KB 201.58 KB 201.71 KB NODE_DEV
ReactDOMForked-prod.js 🔺+0.7% 🔺+0.8% 393.84 KB 396.54 KB 72.27 KB 72.84 KB FB_WWW_PROD
react-dom-server.node.development.js 0.0% 0.0% 137.08 KB 137.08 KB 36.38 KB 36.38 KB NODE_DEV
react-dom.production.min.js 0.0% 0.0% 119.73 KB 119.78 KB 38.54 KB 38.56 KB NODE_PROD
ReactDOMForked-profiling.js +0.7% +0.7% 407.41 KB 410.11 KB 74.74 KB 75.27 KB FB_WWW_PROFILING
react-dom-server.browser.development.js 0.0% 0.0% 143.16 KB 143.16 KB 36.58 KB 36.58 KB UMD_DEV
react-dom-test-utils.production.min.js 0.0% -0.0% 12.83 KB 12.83 KB 5.04 KB 5.04 KB UMD_PROD
ReactDOMTesting-dev.js +0.1% +0.1% 952.11 KB 952.97 KB 213.14 KB 213.3 KB FB_WWW_DEV
ReactDOMTesting-prod.js 0.0% 0.0% 393.4 KB 393.49 KB 73.6 KB 73.62 KB FB_WWW_PROD
react-dom-test-utils.production.min.js 0.0% -0.0% 12.67 KB 12.67 KB 4.96 KB 4.96 KB NODE_PROD
ReactTestUtils-dev.js +0.1% +0.1% 55.55 KB 55.62 KB 15.58 KB 15.6 KB FB_WWW_DEV
react-dom.development.js +0.1% +0.1% 925.84 KB 926.66 KB 204.04 KB 204.19 KB UMD_DEV
react-dom.production.min.js 0.0% -0.0% 119.68 KB 119.74 KB 39.28 KB 39.27 KB UMD_PROD
react-dom.profiling.min.js 0.0% 0.0% 123.59 KB 123.65 KB 40.48 KB 40.5 KB UMD_PROFILING
ReactDOMForked-dev.js +0.4% +0.5% 995.23 KB 999.68 KB 220.38 KB 221.58 KB FB_WWW_DEV
react-dom.profiling.min.js 0.0% 0.0% 123.8 KB 123.85 KB 39.8 KB 39.81 KB NODE_PROFILING
ReactDOM-dev.js +0.5% +0.5% 988.5 KB 992.95 KB 219.44 KB 220.63 KB FB_WWW_DEV
ReactDOM-prod.js 🔺+0.7% 🔺+0.7% 390.31 KB 393.01 KB 71.58 KB 72.12 KB FB_WWW_PROD
ReactDOM-profiling.js +0.7% +0.7% 403.44 KB 406.14 KB 73.99 KB 74.53 KB FB_WWW_PROFILING
ReactDOMServer-dev.js +0.1% +0.1% 145.53 KB 145.61 KB 37.3 KB 37.32 KB FB_WWW_DEV
react-dom-test-utils.development.js 0.0% -0.0% 66.09 KB 66.09 KB 18.23 KB 18.23 KB UMD_DEV

ReactDOM: size: 0.0%, gzip: -0.0%

Size changes (stable)

Generated by 🚫 dangerJS against 46ee6be

@sizebot
Copy link

sizebot commented Aug 20, 2020

Details of bundled changes.

Comparing: 90d212d...46ee6be

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom.development.js +0.1% +0.1% 917.02 KB 917.74 KB 208.09 KB 208.23 KB NODE_DEV
ReactDOMForked-prod.js 🔺+0.7% 🔺+0.8% 382.67 KB 385.37 KB 70.52 KB 71.09 KB FB_WWW_PROD
react-dom-server.node.development.js 0.0% -0.0% 138.59 KB 138.59 KB 36.59 KB 36.59 KB NODE_DEV
react-dom.production.min.js 0.0% 0.0% 124.35 KB 124.4 KB 39.91 KB 39.92 KB NODE_PROD
ReactDOMForked-profiling.js +0.7% +0.8% 396.2 KB 398.9 KB 73 KB 73.56 KB FB_WWW_PROFILING
react-dom-server.browser.development.js 0.0% -0.0% 144.74 KB 144.74 KB 36.78 KB 36.78 KB UMD_DEV
react-dom-server.node.production.min.js 0.0% 0.0% 20.66 KB 20.66 KB 7.65 KB 7.65 KB NODE_PROD
react-dom-test-utils.production.min.js 0.0% -0.0% 12.85 KB 12.85 KB 5.05 KB 5.05 KB UMD_PROD
ReactDOMTesting-dev.js +0.1% +0.1% 924.03 KB 924.89 KB 207.64 KB 207.81 KB FB_WWW_DEV
react-dom-test-utils.development.js 0.0% -0.0% 61.1 KB 61.1 KB 17.75 KB 17.75 KB NODE_DEV
ReactDOMTesting-prod.js 0.0% 0.0% 380.76 KB 380.84 KB 71.53 KB 71.55 KB FB_WWW_PROD
react-dom-unstable-fizz.node.development.js 0.0% -0.1% 5.52 KB 5.52 KB 1.84 KB 1.84 KB NODE_DEV
react-dom-test-utils.production.min.js 0.0% -0.0% 12.68 KB 12.68 KB 4.97 KB 4.97 KB NODE_PROD
react-dom-unstable-fizz.node.production.min.js 0.0% -0.2% 1.17 KB 1.17 KB 666 B 665 B NODE_PROD
react-dom-unstable-fizz.browser.production.min.js 0.0% -0.3% 1.22 KB 1.22 KB 712 B 710 B UMD_PROD
ReactTestUtils-dev.js +0.1% +0.1% 55.55 KB 55.62 KB 15.59 KB 15.61 KB FB_WWW_DEV
react-dom-unstable-fizz.browser.development.js 0.0% -0.1% 4.78 KB 4.78 KB 1.68 KB 1.68 KB NODE_DEV
react-dom.development.js +0.1% +0.1% 963.36 KB 964.17 KB 210.68 KB 210.83 KB UMD_DEV
react-dom-unstable-fizz.browser.production.min.js 0.0% -0.3% 1.01 KB 1.01 KB 616 B 614 B NODE_PROD
react-dom.production.min.js 0.0% 0.0% 124.25 KB 124.3 KB 40.67 KB 40.68 KB UMD_PROD
react-dom.profiling.min.js 0.0% 0.0% 129.51 KB 129.56 KB 42.3 KB 42.3 KB UMD_PROFILING
ReactDOMForked-dev.js +0.5% +0.5% 969.71 KB 974.16 KB 215.58 KB 216.72 KB FB_WWW_DEV
react-dom.profiling.min.js 0.0% 0.0% 129.8 KB 129.85 KB 41.53 KB 41.54 KB NODE_PROFILING
react-dom-server.browser.production.min.js 0.0% -0.0% 20.34 KB 20.34 KB 7.54 KB 7.54 KB UMD_PROD
ReactDOM-dev.js +0.5% +0.5% 962.98 KB 967.43 KB 214.72 KB 215.87 KB FB_WWW_DEV
ReactDOM-prod.js 🔺+0.7% 🔺+0.7% 379.15 KB 381.85 KB 69.9 KB 70.41 KB FB_WWW_PROD
react-dom-server.browser.development.js 0.0% -0.0% 137.32 KB 137.32 KB 36.34 KB 36.34 KB NODE_DEV
ReactDOM-profiling.js +0.7% +0.7% 392.23 KB 394.93 KB 72.28 KB 72.83 KB FB_WWW_PROFILING
react-dom-server.browser.production.min.js 0.0% -0.0% 20.24 KB 20.24 KB 7.5 KB 7.5 KB NODE_PROD
ReactDOMServer-dev.js +0.1% +0.1% 141.5 KB 141.58 KB 36.29 KB 36.31 KB FB_WWW_DEV
react-dom-test-utils.development.js 0.0% -0.0% 66.1 KB 66.1 KB 18.24 KB 18.24 KB UMD_DEV

ReactDOM: size: 0.0%, gzip: -0.0%

Size changes (experimental)

Generated by 🚫 dangerJS against 46ee6be

@gaearon gaearon changed the title (WIP) Attach listeners eagerly Attach Listeners Eagerly to Roots and Portal Containers Aug 21, 2020
@@ -327,6 +329,41 @@ export function listenToNonDelegatedEvent(
}
}

const listeningMarker =
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Want to have a flag, but one per React copy. To guard around cases like "rendering a portal into a div managed by another React".

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please can you explain more about why we need a marker?

Copy link
Collaborator Author

@gaearon gaearon Aug 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to mark a node as processed so we don’t keep adding more and more listeners to it every time. Like you mentioned below.

But we need to mark it with a field that’s local to this copy of React. In case two copies use the same portal container. You don’t want one of them to prevent another from adding listeners.

There’s a test that fails if we don’t do this (although it reproduces this problem mostly by accident due to resetModules and a shared container).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we copy the test and scope it to this use case? It sounds like that test could change and we would lose the regression test here.

if (!enableEagerRootListeners) {
ensureListeningTo(rootContainerElement, propKey, domElement);
} else if (propKey === 'onScroll') {
listenToNonDelegatedEvent('scroll', domElement);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't avoid for scroll since it only bubbles to document.


export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
if (enableEagerRootListeners) {
if ((rootContainerElement: any)[listeningMarker]) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only do this once per container.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we only did this once per event per container? Would that allow us to attach custom events lazily later?

null,
);
}
listenToNativeEvent(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capture phase.


if (enableEagerRootListeners) {
for (let i = 0; i < dependencies.length; i++) {
allNativeEvents.add(dependencies[i]);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make a list once when registering plugins. Then use it for any root. Assumed not to change.

) {
return;
if (!enableEagerRootListeners) {
const eventListenerMap = getEventListenerMap(targetContainer);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't need to do this anymore since it can't happen with eager listeners.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most likely not, but we can always remove this later. Less changes at once make it easier to identify possible issues.

Copy link
Collaborator Author

@gaearon gaearon Aug 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean it doesn’t work unless I remove it because that condition no longer makes sense (we don’t fill the map anymore with React event names).

We could fill the map of course but that would complicate the code and the condition would always be false anyway because the onSelect event would by definition always be there.

@trueadm
Copy link
Contributor

trueadm commented Aug 21, 2020

The downside of this change is that it makes createEventHandle less powerful, as it would only work for known events. However, the list of known events is finite, and we can manually add the missing ones if there is a need. (Maybe it's worth adding a warning if you register an unknown one?)

So does this mean that createEventHandle no longer supports custom events with the changes in this PR?

@gaearon
Copy link
Collaborator Author

gaearon commented Aug 21, 2020

So does this mean that createEventHandle no longer supports custom events with the changes in this PR?

It doesn't support events that are not listed in registered as top level listeners, correct.

However, if there are specific events we want to support, we can add them to the list just for that purpose. (I've checked our existing usages and haven't found any that are being used.)

@trueadm
Copy link
Contributor

trueadm commented Aug 21, 2020

In the case of having a portal on the same node, do we ensure we don't add the event listeners multiple times? i.e. ReactDOM.createPortal(..., document.body)

@gaearon
Copy link
Collaborator Author

gaearon commented Aug 21, 2020

Yeah, there's a check that marks the node as already having the listeners (per copy of React).

@trueadm
Copy link
Contributor

trueadm commented Aug 21, 2020

However, if there are specific events we want to support, we can add them to the list just for that purpose. (I've checked our existing usages and haven't found any that are being used.)

We should definitely add a warning here then. Also, why doens't the custom event test in DOMPluginEventSystem-test.internal.js fail?

This was referenced Mar 15, 2021
koto pushed a commit to koto/react that referenced this pull request Jun 15, 2021
* Failing test for facebook#19608

* Attach Listeners Eagerly to Roots and Portal Containers

* Forbid createEventHandle with custom events

We can't support this without adding more complexity. It's not clear that this is even desirable, as none of our existing use cases need custom events. This API primarily exists as a deprecation strategy for Flare, so I don't think it is important to expand its support beyond what Flare replacement code currently needs. We can later revisit it with a better understanding of the eager/lazy tradeoff but for now let's remove the inconsistency.

* Reduce risk by changing condition only under the flag

Co-authored-by: koba04 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed React Core Team Opened by a member of the React Core Team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Bug: (17.0.0-rc.0) Event propagation through portals is inconsistent
7 participants