Skip to content

Commit 49216d6

Browse files
committed
Attach Listeners Eagerly to Roots and Portal Containers
1 parent c232f0c commit 49216d6

21 files changed

+340
-164
lines changed

packages/react-dom/src/__tests__/ReactDOMEventListener-test.js

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -398,18 +398,49 @@ describe('ReactDOMEventListener', () => {
398398
const originalDocAddEventListener = document.addEventListener;
399399
const originalRootAddEventListener = container.addEventListener;
400400
document.addEventListener = function(type) {
401-
throw new Error(
402-
`Did not expect to add a document-level listener for the "${type}" event.`,
403-
);
401+
switch (type) {
402+
case 'selectionchange':
403+
break;
404+
default:
405+
throw new Error(
406+
`Did not expect to add a document-level listener for the "${type}" event.`,
407+
);
408+
}
404409
};
405-
container.addEventListener = function(type) {
406-
if (type === 'mouseout' || type === 'mouseover') {
407-
// We currently listen to it unconditionally.
410+
container.addEventListener = function(type, fn, options) {
411+
if (options && (options === true || options.capture)) {
408412
return;
409413
}
410-
throw new Error(
411-
`Did not expect to add a root-level listener for the "${type}" event.`,
412-
);
414+
switch (type) {
415+
case 'abort':
416+
case 'canplay':
417+
case 'canplaythrough':
418+
case 'durationchange':
419+
case 'emptied':
420+
case 'encrypted':
421+
case 'ended':
422+
case 'error':
423+
case 'loadeddata':
424+
case 'loadedmetadata':
425+
case 'loadstart':
426+
case 'pause':
427+
case 'play':
428+
case 'playing':
429+
case 'progress':
430+
case 'ratechange':
431+
case 'seeked':
432+
case 'seeking':
433+
case 'stalled':
434+
case 'suspend':
435+
case 'timeupdate':
436+
case 'volumechange':
437+
case 'waiting':
438+
throw new Error(
439+
`Did not expect to add a root-level listener for the "${type}" event.`,
440+
);
441+
default:
442+
break;
443+
}
413444
};
414445

415446
try {

packages/react-dom/src/__tests__/ReactDOMFiber-test.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1040,6 +1040,7 @@ describe('ReactDOMFiber', () => {
10401040
expect(ops).toEqual([]);
10411041
});
10421042

1043+
// @gate enableEagerRootListeners
10431044
it('listens to events that do not exist in the Portal subtree', () => {
10441045
const onClick = jest.fn();
10451046

@@ -1055,7 +1056,7 @@ describe('ReactDOMFiber', () => {
10551056
});
10561057
ref.current.dispatchEvent(event);
10571058

1058-
expect(onClick).toHaveBeenCalledTimes(1); // 0
1059+
expect(onClick).toHaveBeenCalledTimes(1);
10591060
});
10601061

10611062
it('should throw on bad createPortal argument', () => {

packages/react-dom/src/client/ReactDOMComponent.js

Lines changed: 71 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,10 @@ import {validateProperties as validateInputProperties} from '../shared/ReactDOMN
7474
import {validateProperties as validateUnknownProperties} from '../shared/ReactDOMUnknownPropertyHook';
7575
import {REACT_OPAQUE_ID_TYPE} from 'shared/ReactSymbols';
7676

77-
import {enableTrustedTypesIntegration} from 'shared/ReactFeatureFlags';
77+
import {
78+
enableTrustedTypesIntegration,
79+
enableEagerRootListeners,
80+
} from 'shared/ReactFeatureFlags';
7881
import {
7982
listenToReactEvent,
8083
mediaEventTypes,
@@ -260,30 +263,32 @@ export function ensureListeningTo(
260263
reactPropEvent: string,
261264
targetElement: Element | null,
262265
): void {
263-
// If we have a comment node, then use the parent node,
264-
// which should be an element.
265-
const rootContainerElement =
266-
rootContainerInstance.nodeType === COMMENT_NODE
267-
? rootContainerInstance.parentNode
268-
: rootContainerInstance;
269-
if (__DEV__) {
270-
if (
271-
rootContainerElement == null ||
272-
(rootContainerElement.nodeType !== ELEMENT_NODE &&
273-
// This is to support rendering into a ShadowRoot:
274-
rootContainerElement.nodeType !== DOCUMENT_FRAGMENT_NODE)
275-
) {
276-
console.error(
277-
'ensureListeningTo(): received a container that was not an element node. ' +
278-
'This is likely a bug in React. Please file an issue.',
279-
);
266+
if (!enableEagerRootListeners) {
267+
// If we have a comment node, then use the parent node,
268+
// which should be an element.
269+
const rootContainerElement =
270+
rootContainerInstance.nodeType === COMMENT_NODE
271+
? rootContainerInstance.parentNode
272+
: rootContainerInstance;
273+
if (__DEV__) {
274+
if (
275+
rootContainerElement == null ||
276+
(rootContainerElement.nodeType !== ELEMENT_NODE &&
277+
// This is to support rendering into a ShadowRoot:
278+
rootContainerElement.nodeType !== DOCUMENT_FRAGMENT_NODE)
279+
) {
280+
console.error(
281+
'ensureListeningTo(): received a container that was not an element node. ' +
282+
'This is likely a bug in React. Please file an issue.',
283+
);
284+
}
280285
}
286+
listenToReactEvent(
287+
reactPropEvent,
288+
((rootContainerElement: any): Element),
289+
targetElement,
290+
);
281291
}
282-
listenToReactEvent(
283-
reactPropEvent,
284-
((rootContainerElement: any): Element),
285-
targetElement,
286-
);
287292
}
288293

289294
function getOwnerDocumentFromRootContainer(
@@ -364,7 +369,11 @@ function setInitialDOMProperties(
364369
if (__DEV__ && typeof nextProp !== 'function') {
365370
warnForInvalidEventListener(propKey, nextProp);
366371
}
367-
ensureListeningTo(rootContainerElement, propKey, domElement);
372+
if (!enableEagerRootListeners) {
373+
ensureListeningTo(rootContainerElement, propKey, domElement);
374+
} else if (propKey === 'onScroll') {
375+
listenToNonDelegatedEvent('scroll', domElement);
376+
}
368377
}
369378
} else if (nextProp != null) {
370379
setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag);
@@ -573,9 +582,11 @@ export function setInitialProperties(
573582
// We listen to this event in case to ensure emulated bubble
574583
// listeners still fire for the invalid event.
575584
listenToNonDelegatedEvent('invalid', domElement);
576-
// For controlled components we always need to ensure we're listening
577-
// to onChange. Even if there is no listener.
578-
ensureListeningTo(rootContainerElement, 'onChange', domElement);
585+
if (!enableEagerRootListeners) {
586+
// For controlled components we always need to ensure we're listening
587+
// to onChange. Even if there is no listener.
588+
ensureListeningTo(rootContainerElement, 'onChange', domElement);
589+
}
579590
break;
580591
case 'option':
581592
ReactDOMOptionValidateProps(domElement, rawProps);
@@ -587,19 +598,23 @@ export function setInitialProperties(
587598
// We listen to this event in case to ensure emulated bubble
588599
// listeners still fire for the invalid event.
589600
listenToNonDelegatedEvent('invalid', domElement);
590-
// For controlled components we always need to ensure we're listening
591-
// to onChange. Even if there is no listener.
592-
ensureListeningTo(rootContainerElement, 'onChange', domElement);
601+
if (!enableEagerRootListeners) {
602+
// For controlled components we always need to ensure we're listening
603+
// to onChange. Even if there is no listener.
604+
ensureListeningTo(rootContainerElement, 'onChange', domElement);
605+
}
593606
break;
594607
case 'textarea':
595608
ReactDOMTextareaInitWrapperState(domElement, rawProps);
596609
props = ReactDOMTextareaGetHostProps(domElement, rawProps);
597610
// We listen to this event in case to ensure emulated bubble
598611
// listeners still fire for the invalid event.
599612
listenToNonDelegatedEvent('invalid', domElement);
600-
// For controlled components we always need to ensure we're listening
601-
// to onChange. Even if there is no listener.
602-
ensureListeningTo(rootContainerElement, 'onChange', domElement);
613+
if (!enableEagerRootListeners) {
614+
// For controlled components we always need to ensure we're listening
615+
// to onChange. Even if there is no listener.
616+
ensureListeningTo(rootContainerElement, 'onChange', domElement);
617+
}
603618
break;
604619
default:
605620
props = rawProps;
@@ -817,7 +832,11 @@ export function diffProperties(
817832
if (__DEV__ && typeof nextProp !== 'function') {
818833
warnForInvalidEventListener(propKey, nextProp);
819834
}
820-
ensureListeningTo(rootContainerElement, propKey, domElement);
835+
if (!enableEagerRootListeners) {
836+
ensureListeningTo(rootContainerElement, propKey, domElement);
837+
} else if (propKey === 'onScroll') {
838+
listenToNonDelegatedEvent('scroll', domElement);
839+
}
821840
}
822841
if (!updatePayload && lastProp !== nextProp) {
823842
// This is a special case. If any listener updates we need to ensure
@@ -969,9 +988,11 @@ export function diffHydratedProperties(
969988
// We listen to this event in case to ensure emulated bubble
970989
// listeners still fire for the invalid event.
971990
listenToNonDelegatedEvent('invalid', domElement);
972-
// For controlled components we always need to ensure we're listening
973-
// to onChange. Even if there is no listener.
974-
ensureListeningTo(rootContainerElement, 'onChange', domElement);
991+
if (!enableEagerRootListeners) {
992+
// For controlled components we always need to ensure we're listening
993+
// to onChange. Even if there is no listener.
994+
ensureListeningTo(rootContainerElement, 'onChange', domElement);
995+
}
975996
break;
976997
case 'option':
977998
ReactDOMOptionValidateProps(domElement, rawProps);
@@ -981,18 +1002,22 @@ export function diffHydratedProperties(
9811002
// We listen to this event in case to ensure emulated bubble
9821003
// listeners still fire for the invalid event.
9831004
listenToNonDelegatedEvent('invalid', domElement);
984-
// For controlled components we always need to ensure we're listening
985-
// to onChange. Even if there is no listener.
986-
ensureListeningTo(rootContainerElement, 'onChange', domElement);
1005+
if (!enableEagerRootListeners) {
1006+
// For controlled components we always need to ensure we're listening
1007+
// to onChange. Even if there is no listener.
1008+
ensureListeningTo(rootContainerElement, 'onChange', domElement);
1009+
}
9871010
break;
9881011
case 'textarea':
9891012
ReactDOMTextareaInitWrapperState(domElement, rawProps);
9901013
// We listen to this event in case to ensure emulated bubble
9911014
// listeners still fire for the invalid event.
9921015
listenToNonDelegatedEvent('invalid', domElement);
993-
// For controlled components we always need to ensure we're listening
994-
// to onChange. Even if there is no listener.
995-
ensureListeningTo(rootContainerElement, 'onChange', domElement);
1016+
if (!enableEagerRootListeners) {
1017+
// For controlled components we always need to ensure we're listening
1018+
// to onChange. Even if there is no listener.
1019+
ensureListeningTo(rootContainerElement, 'onChange', domElement);
1020+
}
9961021
break;
9971022
}
9981023

@@ -1059,7 +1084,9 @@ export function diffHydratedProperties(
10591084
if (__DEV__ && typeof nextProp !== 'function') {
10601085
warnForInvalidEventListener(propKey, nextProp);
10611086
}
1062-
ensureListeningTo(rootContainerElement, propKey, domElement);
1087+
if (!enableEagerRootListeners) {
1088+
ensureListeningTo(rootContainerElement, propKey, domElement);
1089+
}
10631090
}
10641091
} else if (
10651092
__DEV__ &&

packages/react-dom/src/client/ReactDOMHostConfig.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,13 @@ import {
6767
enableFundamentalAPI,
6868
enableCreateEventHandleAPI,
6969
enableScopeAPI,
70+
enableEagerRootListeners,
7071
} from 'shared/ReactFeatureFlags';
7172
import {HostComponent, HostText} from 'react-reconciler/src/ReactWorkTags';
72-
import {listenToReactEvent} from '../events/DOMPluginEventSystem';
73+
import {
74+
listenToReactEvent,
75+
listenToAllSupportedEvents,
76+
} from '../events/DOMPluginEventSystem';
7377

7478
export type Type = string;
7579
export type Props = {
@@ -1069,7 +1073,11 @@ export function makeOpaqueHydratingObject(
10691073
}
10701074

10711075
export function preparePortalMount(portalInstance: Instance): void {
1072-
listenToReactEvent('onMouseEnter', portalInstance, null);
1076+
if (enableEagerRootListeners) {
1077+
listenToAllSupportedEvents(portalInstance);
1078+
} else {
1079+
listenToReactEvent('onMouseEnter', portalInstance, null);
1080+
}
10731081
}
10741082

10751083
export function prepareScopeUpdate(

packages/react-dom/src/client/ReactDOMRoot.js

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
markContainerAsRoot,
3636
unmarkContainerAsRoot,
3737
} from './ReactDOMComponentTree';
38+
import {listenToAllSupportedEvents} from '../events/DOMPluginEventSystem';
3839
import {eagerlyTrapReplayableEvents} from '../events/ReactDOMEventReplaying';
3940
import {
4041
ELEMENT_NODE,
@@ -51,6 +52,7 @@ import {
5152
registerMutableSourceForHydration,
5253
} from 'react-reconciler/src/ReactFiberReconciler';
5354
import invariant from 'shared/invariant';
55+
import {enableEagerRootListeners} from 'shared/ReactFeatureFlags';
5456
import {
5557
BlockingRoot,
5658
ConcurrentRoot,
@@ -133,19 +135,27 @@ function createRootImpl(
133135
markContainerAsRoot(root.current, container);
134136
const containerNodeType = container.nodeType;
135137

136-
if (hydrate && tag !== LegacyRoot) {
137-
const doc =
138-
containerNodeType === DOCUMENT_NODE ? container : container.ownerDocument;
139-
// We need to cast this because Flow doesn't work
140-
// with the hoisted containerNodeType. If we inline
141-
// it, then Flow doesn't complain. We intentionally
142-
// hoist it to reduce code-size.
143-
eagerlyTrapReplayableEvents(container, ((doc: any): Document));
144-
} else if (
145-
containerNodeType !== DOCUMENT_FRAGMENT_NODE &&
146-
containerNodeType !== DOCUMENT_NODE
147-
) {
148-
ensureListeningTo(container, 'onMouseEnter', null);
138+
if (enableEagerRootListeners) {
139+
const rootContainerElement =
140+
container.nodeType === COMMENT_NODE ? container.parentNode : container;
141+
listenToAllSupportedEvents(rootContainerElement);
142+
} else {
143+
if (hydrate && tag !== LegacyRoot) {
144+
const doc =
145+
containerNodeType === DOCUMENT_NODE
146+
? container
147+
: container.ownerDocument;
148+
// We need to cast this because Flow doesn't work
149+
// with the hoisted containerNodeType. If we inline
150+
// it, then Flow doesn't complain. We intentionally
151+
// hoist it to reduce code-size.
152+
eagerlyTrapReplayableEvents(container, ((doc: any): Document));
153+
} else if (
154+
containerNodeType !== DOCUMENT_FRAGMENT_NODE &&
155+
containerNodeType !== DOCUMENT_NODE
156+
) {
157+
ensureListeningTo(container, 'onMouseEnter', null);
158+
}
149159
}
150160

151161
if (mutableSources) {

0 commit comments

Comments
 (0)