Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 848bb24

Browse files
gaearonkoba04
andauthoredAug 24, 2020
Attach Listeners Eagerly to Roots and Portal Containers (#19659)
* Failing test for #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]>
1 parent 8c9fc4e commit 848bb24

25 files changed

+527
-233
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: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1040,6 +1040,25 @@ describe('ReactDOMFiber', () => {
10401040
expect(ops).toEqual([]);
10411041
});
10421042

1043+
// @gate enableEagerRootListeners
1044+
it('listens to events that do not exist in the Portal subtree', () => {
1045+
const onClick = jest.fn();
1046+
1047+
const ref = React.createRef();
1048+
ReactDOM.render(
1049+
<div onClick={onClick}>
1050+
{ReactDOM.createPortal(<button ref={ref}>click</button>, document.body)}
1051+
</div>,
1052+
container,
1053+
);
1054+
const event = new MouseEvent('click', {
1055+
bubbles: true,
1056+
});
1057+
ref.current.dispatchEvent(event);
1058+
1059+
expect(onClick).toHaveBeenCalledTimes(1);
1060+
});
1061+
10431062
it('should throw on bad createPortal argument', () => {
10441063
expect(() => {
10451064
ReactDOM.createPortal(<div>portal</div>, null);

‎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/ReactDOMEventHandle.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515
} from '../shared/ReactDOMTypes';
1616

1717
import {getEventPriorityForListenerSystem} from '../events/DOMEventProperties';
18+
import {allNativeEvents} from '../events/EventRegistry';
1819
import {
1920
getClosestInstanceFromNode,
2021
getEventHandlerListeners,
@@ -35,6 +36,7 @@ import {IS_EVENT_HANDLE_NON_MANAGED_NODE} from '../events/EventSystemFlags';
3536
import {
3637
enableScopeAPI,
3738
enableCreateEventHandleAPI,
39+
enableEagerRootListeners,
3840
} from 'shared/ReactFeatureFlags';
3941
import invariant from 'shared/invariant';
4042

@@ -178,6 +180,26 @@ export function createEventHandle(
178180
): ReactDOMEventHandle {
179181
if (enableCreateEventHandleAPI) {
180182
const domEventName = ((type: any): DOMEventName);
183+
184+
if (enableEagerRootListeners) {
185+
// We cannot support arbitrary native events with eager root listeners
186+
// because the eager strategy relies on knowing the whole list ahead of time.
187+
// If we wanted to support this, we'd have to add code to keep track
188+
// (or search) for all portal and root containers, and lazily add listeners
189+
// to them whenever we see a previously unknown event. This seems like a lot
190+
// of complexity for something we don't even have a particular use case for.
191+
// Unfortunately, the downside of this invariant is that *removing* a native
192+
// event from the list of known events has now become a breaking change for
193+
// any code relying on the createEventHandle API.
194+
invariant(
195+
allNativeEvents.has(domEventName) ||
196+
domEventName === 'beforeblur' ||
197+
domEventName === 'afterblur',
198+
'Cannot call unstable_createEventHandle with "%s", as it is not an event known to React.',
199+
domEventName,
200+
);
201+
}
202+
181203
let isCapturePhaseListener = false;
182204
let isPassiveListener = undefined; // Undefined means to use the browser default
183205
let listenerPriority;

‎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) {

‎packages/react-dom/src/events/DOMEventProperties.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,8 +201,9 @@ export function getEventPriorityForListenerSystem(
201201
}
202202
if (__DEV__) {
203203
console.warn(
204-
'The event "type" provided to createEventHandle() does not have a known priority type.' +
204+
'The event "%s" provided to createEventHandle() does not have a known priority type.' +
205205
' It is recommended to provide a "priority" option to specify a priority.',
206+
type,
206207
);
207208
}
208209
return ContinuousEvent;

‎packages/react-dom/src/events/DOMPluginEventSystem.js

Lines changed: 84 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import type {ElementListenerMapEntry} from '../client/ReactDOMComponentTree';
2323
import type {EventPriority} from 'shared/ReactTypes';
2424
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
2525

26-
import {registrationNameDependencies} from './EventRegistry';
26+
import {registrationNameDependencies, allNativeEvents} from './EventRegistry';
2727
import {
2828
IS_CAPTURE_PHASE,
2929
IS_EVENT_HANDLE_NON_MANAGED_NODE,
@@ -54,11 +54,13 @@ import {
5454
enableCreateEventHandleAPI,
5555
enableScopeAPI,
5656
enablePassiveEventIntervention,
57+
enableEagerRootListeners,
5758
} from 'shared/ReactFeatureFlags';
5859
import {
5960
invokeGuardedCallbackAndCatchFirstError,
6061
rethrowCaughtError,
6162
} from 'shared/ReactErrorUtils';
63+
import {DOCUMENT_NODE} from '../shared/HTMLNodeType';
6264
import {createEventListenerWrapperWithPriority} from './ReactDOMEventListener';
6365
import {
6466
removeEventListener,
@@ -327,6 +329,41 @@ export function listenToNonDelegatedEvent(
327329
}
328330
}
329331

332+
const listeningMarker =
333+
'_reactListening' +
334+
Math.random()
335+
.toString(36)
336+
.slice(2);
337+
338+
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
339+
if (enableEagerRootListeners) {
340+
if ((rootContainerElement: any)[listeningMarker]) {
341+
// Performance optimization: don't iterate through events
342+
// for the same portal container or root node more than once.
343+
// TODO: once we remove the flag, we may be able to also
344+
// remove some of the bookkeeping maps used for laziness.
345+
return;
346+
}
347+
(rootContainerElement: any)[listeningMarker] = true;
348+
allNativeEvents.forEach(domEventName => {
349+
if (!nonDelegatedEvents.has(domEventName)) {
350+
listenToNativeEvent(
351+
domEventName,
352+
false,
353+
((rootContainerElement: any): Element),
354+
null,
355+
);
356+
}
357+
listenToNativeEvent(
358+
domEventName,
359+
true,
360+
((rootContainerElement: any): Element),
361+
null,
362+
);
363+
});
364+
}
365+
}
366+
330367
export function listenToNativeEvent(
331368
domEventName: DOMEventName,
332369
isCapturePhaseListener: boolean,
@@ -337,10 +374,14 @@ export function listenToNativeEvent(
337374
eventSystemFlags?: EventSystemFlags = 0,
338375
): void {
339376
let target = rootContainerElement;
377+
340378
// selectionchange needs to be attached to the document
341379
// otherwise it won't capture incoming events that are only
342380
// triggered on the document directly.
343-
if (domEventName === 'selectionchange') {
381+
if (
382+
domEventName === 'selectionchange' &&
383+
(rootContainerElement: any).nodeType !== DOCUMENT_NODE
384+
) {
344385
target = (rootContainerElement: any).ownerDocument;
345386
}
346387
if (enablePassiveEventIntervention && isPassiveListener === undefined) {
@@ -426,48 +467,50 @@ export function listenToReactEvent(
426467
rootContainerElement: Element,
427468
targetElement: Element | null,
428469
): void {
429-
const dependencies = registrationNameDependencies[reactEvent];
430-
const dependenciesLength = dependencies.length;
431-
// If the dependencies length is 1, that means we're not using a polyfill
432-
// plugin like ChangeEventPlugin, BeforeInputPlugin, EnterLeavePlugin
433-
// and SelectEventPlugin. We always use the native bubble event phase for
434-
// these plugins and emulate two phase event dispatching. SimpleEventPlugin
435-
// always only has a single dependency and SimpleEventPlugin events also
436-
// use either the native capture event phase or bubble event phase, there
437-
// is no emulation (except for focus/blur, but that will be removed soon).
438-
const isPolyfillEventPlugin = dependenciesLength !== 1;
470+
if (!enableEagerRootListeners) {
471+
const dependencies = registrationNameDependencies[reactEvent];
472+
const dependenciesLength = dependencies.length;
473+
// If the dependencies length is 1, that means we're not using a polyfill
474+
// plugin like ChangeEventPlugin, BeforeInputPlugin, EnterLeavePlugin
475+
// and SelectEventPlugin. We always use the native bubble event phase for
476+
// these plugins and emulate two phase event dispatching. SimpleEventPlugin
477+
// always only has a single dependency and SimpleEventPlugin events also
478+
// use either the native capture event phase or bubble event phase, there
479+
// is no emulation (except for focus/blur, but that will be removed soon).
480+
const isPolyfillEventPlugin = dependenciesLength !== 1;
439481

440-
if (isPolyfillEventPlugin) {
441-
const listenerMap = getEventListenerMap(rootContainerElement);
442-
// For optimization, we register plugins on the listener map, so we
443-
// don't need to check each of their dependencies each time.
444-
if (!listenerMap.has(reactEvent)) {
445-
listenerMap.set(reactEvent, null);
446-
for (let i = 0; i < dependenciesLength; i++) {
447-
listenToNativeEvent(
448-
dependencies[i],
449-
false,
450-
rootContainerElement,
451-
targetElement,
452-
);
482+
if (isPolyfillEventPlugin) {
483+
const listenerMap = getEventListenerMap(rootContainerElement);
484+
// For optimization, we register plugins on the listener map, so we
485+
// don't need to check each of their dependencies each time.
486+
if (!listenerMap.has(reactEvent)) {
487+
listenerMap.set(reactEvent, null);
488+
for (let i = 0; i < dependenciesLength; i++) {
489+
listenToNativeEvent(
490+
dependencies[i],
491+
false,
492+
rootContainerElement,
493+
targetElement,
494+
);
495+
}
453496
}
497+
} else {
498+
const isCapturePhaseListener =
499+
reactEvent.substr(-7) === 'Capture' &&
500+
// Edge case: onGotPointerCapture and onLostPointerCapture
501+
// end with "Capture" but that's part of their event names.
502+
// The Capture versions would end with CaptureCapture.
503+
// So we have to check against that.
504+
// This check works because none of the events we support
505+
// end with "Pointer".
506+
reactEvent.substr(-14, 7) !== 'Pointer';
507+
listenToNativeEvent(
508+
dependencies[0],
509+
isCapturePhaseListener,
510+
rootContainerElement,
511+
targetElement,
512+
);
454513
}
455-
} else {
456-
const isCapturePhaseListener =
457-
reactEvent.substr(-7) === 'Capture' &&
458-
// Edge case: onGotPointerCapture and onLostPointerCapture
459-
// end with "Capture" but that's part of their event names.
460-
// The Capture versions would end with CaptureCapture.
461-
// So we have to check against that.
462-
// This check works because none of the events we support
463-
// end with "Pointer".
464-
reactEvent.substr(-14, 7) !== 'Pointer';
465-
listenToNativeEvent(
466-
dependencies[0],
467-
isCapturePhaseListener,
468-
rootContainerElement,
469-
targetElement,
470-
);
471514
}
472515
}
473516

‎packages/react-dom/src/events/EventRegistry.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99

1010
import type {DOMEventName} from './DOMEventNames';
1111

12+
import {enableEagerRootListeners} from 'shared/ReactFeatureFlags';
13+
14+
export const allNativeEvents: Set<DOMEventName> = new Set();
15+
1216
/**
1317
* Mapping from registration name to event name
1418
*/
@@ -25,15 +29,15 @@ export const possibleRegistrationNames = __DEV__ ? {} : (null: any);
2529

2630
export function registerTwoPhaseEvent(
2731
registrationName: string,
28-
dependencies: ?Array<DOMEventName>,
32+
dependencies: Array<DOMEventName>,
2933
): void {
3034
registerDirectEvent(registrationName, dependencies);
3135
registerDirectEvent(registrationName + 'Capture', dependencies);
3236
}
3337

3438
export function registerDirectEvent(
3539
registrationName: string,
36-
dependencies: ?Array<DOMEventName>,
40+
dependencies: Array<DOMEventName>,
3741
) {
3842
if (__DEV__) {
3943
if (registrationNameDependencies[registrationName]) {
@@ -55,4 +59,10 @@ export function registerDirectEvent(
5559
possibleRegistrationNames.ondblclick = registrationName;
5660
}
5761
}
62+
63+
if (enableEagerRootListeners) {
64+
for (let i = 0; i < dependencies.length; i++) {
65+
allNativeEvents.add(dependencies[i]);
66+
}
67+
}
5868
}

‎packages/react-dom/src/events/ReactDOMEventListener.js

Lines changed: 46 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
import {HostRoot, SuspenseComponent} from 'react-reconciler/src/ReactWorkTags';
3333
import {
3434
type EventSystemFlags,
35+
IS_CAPTURE_PHASE,
3536
IS_LEGACY_FB_SUPPORT_MODE,
3637
} from './EventSystemFlags';
3738

@@ -40,6 +41,7 @@ import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree';
4041

4142
import {
4243
enableLegacyFBSupport,
44+
enableEagerRootListeners,
4345
decoupleUpdatePriorityFromScheduler,
4446
} from 'shared/ReactFeatureFlags';
4547
import {
@@ -191,7 +193,21 @@ export function dispatchEvent(
191193
if (!_enabled) {
192194
return;
193195
}
194-
if (hasQueuedDiscreteEvents() && isReplayableDiscreteEvent(domEventName)) {
196+
let allowReplay = true;
197+
if (enableEagerRootListeners) {
198+
// TODO: replaying capture phase events is currently broken
199+
// because we used to do it during top-level native bubble handlers
200+
// but now we use different bubble and capture handlers.
201+
// In eager mode, we attach capture listeners early, so we need
202+
// to filter them out until we fix the logic to handle them correctly.
203+
// This could've been outside the flag but I put it inside to reduce risk.
204+
allowReplay = (eventSystemFlags & IS_CAPTURE_PHASE) === 0;
205+
}
206+
if (
207+
allowReplay &&
208+
hasQueuedDiscreteEvents() &&
209+
isReplayableDiscreteEvent(domEventName)
210+
) {
195211
// If we already have a queue of discrete events, and this is another discrete
196212
// event, then we can't dispatch it regardless of its target, since they
197213
// need to dispatch in order.
@@ -214,38 +230,40 @@ export function dispatchEvent(
214230

215231
if (blockedOn === null) {
216232
// We successfully dispatched this event.
217-
clearIfContinuousEvent(domEventName, nativeEvent);
218-
return;
219-
}
220-
221-
if (isReplayableDiscreteEvent(domEventName)) {
222-
// This this to be replayed later once the target is available.
223-
queueDiscreteEvent(
224-
blockedOn,
225-
domEventName,
226-
eventSystemFlags,
227-
targetContainer,
228-
nativeEvent,
229-
);
233+
if (allowReplay) {
234+
clearIfContinuousEvent(domEventName, nativeEvent);
235+
}
230236
return;
231237
}
232238

233-
if (
234-
queueIfContinuousEvent(
235-
blockedOn,
236-
domEventName,
237-
eventSystemFlags,
238-
targetContainer,
239-
nativeEvent,
240-
)
241-
) {
242-
return;
239+
if (allowReplay) {
240+
if (isReplayableDiscreteEvent(domEventName)) {
241+
// This this to be replayed later once the target is available.
242+
queueDiscreteEvent(
243+
blockedOn,
244+
domEventName,
245+
eventSystemFlags,
246+
targetContainer,
247+
nativeEvent,
248+
);
249+
return;
250+
}
251+
if (
252+
queueIfContinuousEvent(
253+
blockedOn,
254+
domEventName,
255+
eventSystemFlags,
256+
targetContainer,
257+
nativeEvent,
258+
)
259+
) {
260+
return;
261+
}
262+
// We need to clear only if we didn't queue because
263+
// queueing is accummulative.
264+
clearIfContinuousEvent(domEventName, nativeEvent);
243265
}
244266

245-
// We need to clear only if we didn't queue because
246-
// queueing is accummulative.
247-
clearIfContinuousEvent(domEventName, nativeEvent);
248-
249267
// This is not replayable so we'll invoke it but without a target,
250268
// in case the event system needs to trace it.
251269
dispatchEventForPluginEventSystem(

‎packages/react-dom/src/events/ReactDOMEventReplaying.js

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ import type {EventSystemFlags} from './EventSystemFlags';
1414
import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
1515
import type {LanePriority} from 'react-reconciler/src/ReactFiberLane';
1616

17-
import {enableSelectiveHydration} from 'shared/ReactFeatureFlags';
17+
import {
18+
enableSelectiveHydration,
19+
enableEagerRootListeners,
20+
} from 'shared/ReactFeatureFlags';
1821
import {
1922
unstable_runWithPriority as runWithPriority,
2023
unstable_scheduleCallback as scheduleCallback,
@@ -177,21 +180,27 @@ function trapReplayableEventForContainer(
177180
domEventName: DOMEventName,
178181
container: Container,
179182
) {
180-
listenToNativeEvent(domEventName, false, ((container: any): Element), null);
183+
// When the flag is on, we do this in a unified codepath elsewhere.
184+
if (!enableEagerRootListeners) {
185+
listenToNativeEvent(domEventName, false, ((container: any): Element), null);
186+
}
181187
}
182188

183189
export function eagerlyTrapReplayableEvents(
184190
container: Container,
185191
document: Document,
186192
) {
187-
// Discrete
188-
discreteReplayableEvents.forEach(domEventName => {
189-
trapReplayableEventForContainer(domEventName, container);
190-
});
191-
// Continuous
192-
continuousReplayableEvents.forEach(domEventName => {
193-
trapReplayableEventForContainer(domEventName, container);
194-
});
193+
// When the flag is on, we do this in a unified codepath elsewhere.
194+
if (!enableEagerRootListeners) {
195+
// Discrete
196+
discreteReplayableEvents.forEach(domEventName => {
197+
trapReplayableEventForContainer(domEventName, container);
198+
});
199+
// Continuous
200+
continuousReplayableEvents.forEach(domEventName => {
201+
trapReplayableEventForContainer(domEventName, container);
202+
});
203+
}
195204
}
196205

197206
function createQueuedReplayableEvent(

‎packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js

Lines changed: 138 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -2401,85 +2401,97 @@ describe('DOMPluginEventSystem', () => {
24012401
);
24022402

24032403
let setCustomEventHandle;
2404+
if (gate(flags => flags.enableEagerRootListeners)) {
2405+
// With eager listeners, supporting custom events via this API doesn't make sense
2406+
// because we can't know a full list of them ahead of time. Let's check we throw
2407+
// since otherwise we'd end up with inconsistent behavior, like no portal bubbling.
2408+
expect(() => {
2409+
setCustomEventHandle = ReactDOM.unstable_createEventHandle(
2410+
'custom-event',
2411+
);
2412+
}).toThrow(
2413+
'Cannot call unstable_createEventHandle with "custom-event", as it is not an event known to React.',
2414+
);
2415+
} else {
2416+
// Test that we get a warning when we don't provide an explicit priority
2417+
expect(() => {
2418+
setCustomEventHandle = ReactDOM.unstable_createEventHandle(
2419+
'custom-event',
2420+
);
2421+
}).toWarnDev(
2422+
'Warning: The event "custom-event" provided to createEventHandle() does not have a known priority type. ' +
2423+
'It is recommended to provide a "priority" option to specify a priority.',
2424+
{withoutStack: true},
2425+
);
24042426

2405-
// Test that we get a warning when we don't provide an explicit priority
2406-
expect(() => {
24072427
setCustomEventHandle = ReactDOM.unstable_createEventHandle(
24082428
'custom-event',
2429+
{
2430+
priority: 0, // Discrete
2431+
},
24092432
);
2410-
}).toWarnDev(
2411-
'Warning: The event "type" provided to createEventHandle() does not have a known priority type. ' +
2412-
'It is recommended to provide a "priority" option to specify a priority.',
2413-
{withoutStack: true},
2414-
);
24152433

2416-
setCustomEventHandle = ReactDOM.unstable_createEventHandle(
2417-
'custom-event',
2418-
{
2419-
priority: 0, // Discrete
2420-
},
2421-
);
2422-
2423-
const setCustomCaptureHandle = ReactDOM.unstable_createEventHandle(
2424-
'custom-event',
2425-
{
2426-
capture: true,
2427-
priority: 0, // Discrete
2428-
},
2429-
);
2434+
const setCustomCaptureHandle = ReactDOM.unstable_createEventHandle(
2435+
'custom-event',
2436+
{
2437+
capture: true,
2438+
priority: 0, // Discrete
2439+
},
2440+
);
24302441

2431-
function Test() {
2432-
React.useEffect(() => {
2433-
const clearCustom1 = setCustomEventHandle(
2434-
buttonRef.current,
2435-
onCustomEvent,
2436-
);
2437-
const clearCustom2 = setCustomCaptureHandle(
2438-
buttonRef.current,
2439-
onCustomEventCapture,
2440-
);
2441-
const clearCustom3 = setCustomEventHandle(
2442-
divRef.current,
2443-
onCustomEvent,
2444-
);
2445-
const clearCustom4 = setCustomCaptureHandle(
2446-
divRef.current,
2447-
onCustomEventCapture,
2448-
);
2442+
const Test = () => {
2443+
React.useEffect(() => {
2444+
const clearCustom1 = setCustomEventHandle(
2445+
buttonRef.current,
2446+
onCustomEvent,
2447+
);
2448+
const clearCustom2 = setCustomCaptureHandle(
2449+
buttonRef.current,
2450+
onCustomEventCapture,
2451+
);
2452+
const clearCustom3 = setCustomEventHandle(
2453+
divRef.current,
2454+
onCustomEvent,
2455+
);
2456+
const clearCustom4 = setCustomCaptureHandle(
2457+
divRef.current,
2458+
onCustomEventCapture,
2459+
);
24492460

2450-
return () => {
2451-
clearCustom1();
2452-
clearCustom2();
2453-
clearCustom3();
2454-
clearCustom4();
2455-
};
2456-
});
2461+
return () => {
2462+
clearCustom1();
2463+
clearCustom2();
2464+
clearCustom3();
2465+
clearCustom4();
2466+
};
2467+
});
24572468

2458-
return (
2459-
<button ref={buttonRef}>
2460-
<div ref={divRef}>Click me!</div>
2461-
</button>
2462-
);
2463-
}
2469+
return (
2470+
<button ref={buttonRef}>
2471+
<div ref={divRef}>Click me!</div>
2472+
</button>
2473+
);
2474+
};
24642475

2465-
ReactDOM.render(<Test />, container);
2466-
Scheduler.unstable_flushAll();
2476+
ReactDOM.render(<Test />, container);
2477+
Scheduler.unstable_flushAll();
24672478

2468-
const buttonElement = buttonRef.current;
2469-
dispatchEvent(buttonElement, 'custom-event');
2470-
expect(onCustomEvent).toHaveBeenCalledTimes(1);
2471-
expect(onCustomEventCapture).toHaveBeenCalledTimes(1);
2472-
expect(log[0]).toEqual(['capture', buttonElement]);
2473-
expect(log[1]).toEqual(['bubble', buttonElement]);
2479+
const buttonElement = buttonRef.current;
2480+
dispatchEvent(buttonElement, 'custom-event');
2481+
expect(onCustomEvent).toHaveBeenCalledTimes(1);
2482+
expect(onCustomEventCapture).toHaveBeenCalledTimes(1);
2483+
expect(log[0]).toEqual(['capture', buttonElement]);
2484+
expect(log[1]).toEqual(['bubble', buttonElement]);
24742485

2475-
const divElement = divRef.current;
2476-
dispatchEvent(divElement, 'custom-event');
2477-
expect(onCustomEvent).toHaveBeenCalledTimes(3);
2478-
expect(onCustomEventCapture).toHaveBeenCalledTimes(3);
2479-
expect(log[2]).toEqual(['capture', buttonElement]);
2480-
expect(log[3]).toEqual(['capture', divElement]);
2481-
expect(log[4]).toEqual(['bubble', divElement]);
2482-
expect(log[5]).toEqual(['bubble', buttonElement]);
2486+
const divElement = divRef.current;
2487+
dispatchEvent(divElement, 'custom-event');
2488+
expect(onCustomEvent).toHaveBeenCalledTimes(3);
2489+
expect(onCustomEventCapture).toHaveBeenCalledTimes(3);
2490+
expect(log[2]).toEqual(['capture', buttonElement]);
2491+
expect(log[3]).toEqual(['capture', divElement]);
2492+
expect(log[4]).toEqual(['bubble', divElement]);
2493+
expect(log[5]).toEqual(['bubble', buttonElement]);
2494+
}
24832495
});
24842496

24852497
// @gate experimental
@@ -2823,6 +2835,64 @@ describe('DOMPluginEventSystem', () => {
28232835
expect(log[5]).toEqual(['bubble', buttonElement]);
28242836
});
28252837

2838+
// @gate experimental && enableEagerRootListeners
2839+
it('propagates known createEventHandle events through portals without inner listeners', () => {
2840+
const buttonRef = React.createRef();
2841+
const divRef = React.createRef();
2842+
const log = [];
2843+
const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
2844+
const onClickCapture = jest.fn(e =>
2845+
log.push(['capture', e.currentTarget]),
2846+
);
2847+
const setClick = ReactDOM.unstable_createEventHandle('click');
2848+
const setClickCapture = ReactDOM.unstable_createEventHandle(
2849+
'click',
2850+
{
2851+
capture: true,
2852+
},
2853+
);
2854+
2855+
const portalElement = document.createElement('div');
2856+
document.body.appendChild(portalElement);
2857+
2858+
function Child() {
2859+
return <div ref={divRef}>Click me!</div>;
2860+
}
2861+
2862+
function Parent() {
2863+
React.useEffect(() => {
2864+
const clear1 = setClick(buttonRef.current, onClick);
2865+
const clear2 = setClickCapture(
2866+
buttonRef.current,
2867+
onClickCapture,
2868+
);
2869+
return () => {
2870+
clear1();
2871+
clear2();
2872+
};
2873+
});
2874+
2875+
return (
2876+
<button ref={buttonRef}>
2877+
{ReactDOM.createPortal(<Child />, portalElement)}
2878+
</button>
2879+
);
2880+
}
2881+
2882+
ReactDOM.render(<Parent />, container);
2883+
Scheduler.unstable_flushAll();
2884+
2885+
const divElement = divRef.current;
2886+
const buttonElement = buttonRef.current;
2887+
dispatchClickEvent(divElement);
2888+
expect(onClick).toHaveBeenCalledTimes(1);
2889+
expect(onClickCapture).toHaveBeenCalledTimes(1);
2890+
expect(log[0]).toEqual(['capture', buttonElement]);
2891+
expect(log[1]).toEqual(['bubble', buttonElement]);
2892+
2893+
document.body.removeChild(portalElement);
2894+
});
2895+
28262896
describe('Compatibility with Scopes API', () => {
28272897
beforeEach(() => {
28282898
jest.resetModules();

‎packages/react-dom/src/events/plugins/SelectEventPlugin.js

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {canUseDOM} from 'shared/ExecutionEnvironment';
1616
import {SyntheticEvent} from '../../events/SyntheticEvent';
1717
import isTextInputElement from '../isTextInputElement';
1818
import shallowEqual from 'shared/shallowEqual';
19+
import {enableEagerRootListeners} from 'shared/ReactFeatureFlags';
1920

2021
import {registerTwoPhaseEvent} from '../EventRegistry';
2122
import getActiveElement from '../../client/getActiveElement';
@@ -152,19 +153,21 @@ function extractEvents(
152153
eventSystemFlags: EventSystemFlags,
153154
targetContainer: EventTarget,
154155
) {
155-
const eventListenerMap = getEventListenerMap(targetContainer);
156-
// Track whether all listeners exists for this plugin. If none exist, we do
157-
// not extract events. See #3639.
158-
if (
159-
// If we are handling selectionchange, then we don't need to
160-
// check for the other dependencies, as selectionchange is only
161-
// event attached from the onChange plugin and we don't expose an
162-
// onSelectionChange event from React.
163-
domEventName !== 'selectionchange' &&
164-
!eventListenerMap.has('onSelect') &&
165-
!eventListenerMap.has('onSelectCapture')
166-
) {
167-
return;
156+
if (!enableEagerRootListeners) {
157+
const eventListenerMap = getEventListenerMap(targetContainer);
158+
// Track whether all listeners exists for this plugin. If none exist, we do
159+
// not extract events. See #3639.
160+
if (
161+
// If we are handling selectionchange, then we don't need to
162+
// check for the other dependencies, as selectionchange is only
163+
// event attached from the onChange plugin and we don't expose an
164+
// onSelectionChange event from React.
165+
domEventName !== 'selectionchange' &&
166+
!eventListenerMap.has('onSelect') &&
167+
!eventListenerMap.has('onSelectCapture')
168+
) {
169+
return;
170+
}
168171
}
169172

170173
const targetNode = targetInst ? getNodeFromInstance(targetInst) : window;

‎packages/react-dom/src/events/plugins/__tests__/SimpleEventPlugin-test.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -538,7 +538,18 @@ describe('SimpleEventPlugin', function() {
538538
);
539539

540540
if (gate(flags => flags.enablePassiveEventIntervention)) {
541-
expect(passiveEvents).toEqual(['touchstart', 'touchmove', 'wheel']);
541+
if (gate(flags => flags.enableEagerRootListeners)) {
542+
expect(passiveEvents).toEqual([
543+
'touchstart',
544+
'touchstart',
545+
'touchmove',
546+
'touchmove',
547+
'wheel',
548+
'wheel',
549+
]);
550+
} else {
551+
expect(passiveEvents).toEqual(['touchstart', 'touchmove', 'wheel']);
552+
}
542553
} else {
543554
expect(passiveEvents).toEqual([]);
544555
}

‎packages/shared/ReactFeatureFlags.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,5 @@ export const enableDiscreteEventFlushingChange = false;
136136
// https://github.com/facebook/react/pull/19654
137137
export const enablePassiveEventIntervention = true;
138138
export const disableOnScrollBubbling = true;
139+
140+
export const enableEagerRootListeners = false;

‎packages/shared/forks/ReactFeatureFlags.native-fb.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export const deferRenderPhaseUpdateToNextBatch = true;
5151
export const decoupleUpdatePriorityFromScheduler = false;
5252
export const enableDiscreteEventFlushingChange = false;
5353
export const enablePassiveEventIntervention = true;
54+
export const enableEagerRootListeners = false;
5455

5556
// Flow magic to verify the exports of this file match the original version.
5657
// eslint-disable-next-line no-unused-vars

‎packages/shared/forks/ReactFeatureFlags.native-oss.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export const deferRenderPhaseUpdateToNextBatch = true;
5050
export const decoupleUpdatePriorityFromScheduler = false;
5151
export const enableDiscreteEventFlushingChange = false;
5252
export const enablePassiveEventIntervention = true;
53+
export const enableEagerRootListeners = false;
5354

5455
// Flow magic to verify the exports of this file match the original version.
5556
// eslint-disable-next-line no-unused-vars

‎packages/shared/forks/ReactFeatureFlags.test-renderer.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export const deferRenderPhaseUpdateToNextBatch = true;
5050
export const decoupleUpdatePriorityFromScheduler = false;
5151
export const enableDiscreteEventFlushingChange = false;
5252
export const enablePassiveEventIntervention = true;
53+
export const enableEagerRootListeners = false;
5354

5455
// Flow magic to verify the exports of this file match the original version.
5556
// eslint-disable-next-line no-unused-vars

‎packages/shared/forks/ReactFeatureFlags.test-renderer.native.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export const deferRenderPhaseUpdateToNextBatch = true;
4949
export const decoupleUpdatePriorityFromScheduler = false;
5050
export const enableDiscreteEventFlushingChange = false;
5151
export const enablePassiveEventIntervention = true;
52+
export const enableEagerRootListeners = false;
5253

5354
// Flow magic to verify the exports of this file match the original version.
5455
// eslint-disable-next-line no-unused-vars

‎packages/shared/forks/ReactFeatureFlags.test-renderer.www.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export const deferRenderPhaseUpdateToNextBatch = true;
5050
export const decoupleUpdatePriorityFromScheduler = false;
5151
export const enableDiscreteEventFlushingChange = false;
5252
export const enablePassiveEventIntervention = true;
53+
export const enableEagerRootListeners = false;
5354

5455
// Flow magic to verify the exports of this file match the original version.
5556
// eslint-disable-next-line no-unused-vars

‎packages/shared/forks/ReactFeatureFlags.testing.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export const deferRenderPhaseUpdateToNextBatch = true;
5050
export const decoupleUpdatePriorityFromScheduler = false;
5151
export const enableDiscreteEventFlushingChange = false;
5252
export const enablePassiveEventIntervention = true;
53+
export const enableEagerRootListeners = false;
5354

5455
// Flow magic to verify the exports of this file match the original version.
5556
// eslint-disable-next-line no-unused-vars

‎packages/shared/forks/ReactFeatureFlags.testing.www.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export const deferRenderPhaseUpdateToNextBatch = true;
5050
export const decoupleUpdatePriorityFromScheduler = false;
5151
export const enableDiscreteEventFlushingChange = true;
5252
export const enablePassiveEventIntervention = true;
53+
export const enableEagerRootListeners = false;
5354

5455
// Flow magic to verify the exports of this file match the original version.
5556
// eslint-disable-next-line no-unused-vars

‎packages/shared/forks/ReactFeatureFlags.www-dynamic.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export const decoupleUpdatePriorityFromScheduler = __VARIANT__;
2121
export const skipUnmountedBoundaries = __VARIANT__;
2222
export const enablePassiveEventIntervention = __VARIANT__;
2323
export const disableOnScrollBubbling = __VARIANT__;
24+
export const enableEagerRootListeners = __VARIANT__;
2425

2526
// Enable this flag to help with concurrent mode debugging.
2627
// It logs information to the console about React scheduling, rendering, and commit phases.

‎packages/shared/forks/ReactFeatureFlags.www.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export const {
2929
skipUnmountedBoundaries,
3030
enablePassiveEventIntervention,
3131
disableOnScrollBubbling,
32+
enableEagerRootListeners,
3233
} = dynamicFeatureFlags;
3334

3435
// On WWW, __EXPERIMENTAL__ is used for a new modern build.

‎scripts/error-codes/codes.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,5 +360,6 @@
360360
"368": "ReactDOM.createEventHandle: setListener called on an invalid target. Provide a valid EventTarget or an element managed by React.",
361361
"369": "ReactDOM.createEventHandle: setter called on an invalid target. Provide a valid EventTarget or an element managed by React.",
362362
"370": "ReactDOM.createEventHandle: setter called with an invalid callback. The callback must be a function.",
363-
"371": "Text string must be rendered within a <Text> component.\n\nText: %s"
363+
"371": "Text string must be rendered within a <Text> component.\n\nText: %s",
364+
"372": "Cannot call unstable_createEventHandle with \"%s\", as it is not an event known to React."
364365
}

0 commit comments

Comments
 (0)
Please sign in to comment.