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 <koba0004@gmail.com>
Dan Abramov committed
Aug 24, 2020 at 16:50 UTC
848bb2426e44606e0a55dfe44c7b3ece33772485
25 files changed
+527
-233
packages/react-dom/src/__tests__/ReactDOMEventListener-test.js
+40
-9
@@ -398,18 +398,49 @@ describe('ReactDOMEventListener', () => {
398
const originalDocAddEventListener = document.addEventListener;
399
const originalRootAddEventListener = container.addEventListener;
400
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
+ }
409
};
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)) {
412
return;
413
}
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
+ }
444
};
445
446
try {
packages/react-dom/src/__tests__/ReactDOMFiber-test.js
+19
@@ -1040,6 +1040,25 @@ describe('ReactDOMFiber', () => {
1040
expect(ops).toEqual([]);
1041
});
1042
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
+
1062
it('should throw on bad createPortal argument', () => {
1063
expect(() => {
1064
ReactDOM.createPortal(<div>portal</div>, null);
packages/react-dom/src/client/ReactDOMComponent.js
+71
-44
@@ -74,7 +74,10 @@ import {validateProperties as validateInputProperties} from '../shared/ReactDOMN
74
import {validateProperties as validateUnknownProperties} from '../shared/ReactDOMUnknownPropertyHook';
75
import {REACT_OPAQUE_ID_TYPE} from 'shared/ReactSymbols';
76
77
-import {enableTrustedTypesIntegration} from 'shared/ReactFeatureFlags';
77
+import {
78
+ enableTrustedTypesIntegration,
79
+ enableEagerRootListeners,
80
+} from 'shared/ReactFeatureFlags';
81
import {
82
listenToReactEvent,
83
mediaEventTypes,
@@ -260,30 +263,32 @@ export function ensureListeningTo(
263
reactPropEvent: string,
264
targetElement: Element | null,
265
): 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
+ }
285
}
286
+ listenToReactEvent(
287
+ reactPropEvent,
288
+ ((rootContainerElement: any): Element),
289
+ targetElement,
290
+ );
291
}
282
- listenToReactEvent(
283
- reactPropEvent,
284
- ((rootContainerElement: any): Element),
285
- targetElement,
286
- );
292
}
293
294
function getOwnerDocumentFromRootContainer(
@@ -364,7 +369,11 @@ function setInitialDOMProperties(
369
if (__DEV__ && typeof nextProp !== 'function') {
370
warnForInvalidEventListener(propKey, nextProp);
371
}
367
- ensureListeningTo(rootContainerElement, propKey, domElement);
372
+ if (!enableEagerRootListeners) {
373
+ ensureListeningTo(rootContainerElement, propKey, domElement);
374
+ } else if (propKey === 'onScroll') {
375
+ listenToNonDelegatedEvent('scroll', domElement);
376
+ }
377
}
378
} else if (nextProp != null) {
379
setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag);
@@ -573,9 +582,11 @@ export function setInitialProperties(
582
// We listen to this event in case to ensure emulated bubble
583
// listeners still fire for the invalid event.
584
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
+ }
590
break;
591
case 'option':
592
ReactDOMOptionValidateProps(domElement, rawProps);
@@ -587,9 +598,11 @@ export function setInitialProperties(
598
// We listen to this event in case to ensure emulated bubble
599
// listeners still fire for the invalid event.
600
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
+ }
606
break;
607
case 'textarea':
608
ReactDOMTextareaInitWrapperState(domElement, rawProps);
@@ -597,9 +610,11 @@ export function setInitialProperties(
610
// We listen to this event in case to ensure emulated bubble
611
// listeners still fire for the invalid event.
612
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
+ }
618
break;
619
default:
620
props = rawProps;
@@ -817,7 +832,11 @@ export function diffProperties(
832
if (__DEV__ && typeof nextProp !== 'function') {
833
warnForInvalidEventListener(propKey, nextProp);
834
}
820
- ensureListeningTo(rootContainerElement, propKey, domElement);
835
+ if (!enableEagerRootListeners) {
836
+ ensureListeningTo(rootContainerElement, propKey, domElement);
837
+ } else if (propKey === 'onScroll') {
838
+ listenToNonDelegatedEvent('scroll', domElement);
839
+ }
840
}
841
if (!updatePayload && lastProp !== nextProp) {
842
// This is a special case. If any listener updates we need to ensure
@@ -969,9 +988,11 @@ export function diffHydratedProperties(
988
// We listen to this event in case to ensure emulated bubble
989
// listeners still fire for the invalid event.
990
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
+ }
996
break;
997
case 'option':
998
ReactDOMOptionValidateProps(domElement, rawProps);
@@ -981,18 +1002,22 @@ export function diffHydratedProperties(
1002
// We listen to this event in case to ensure emulated bubble
1003
// listeners still fire for the invalid event.
1004
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
+ }
1010
break;
1011
case 'textarea':
1012
ReactDOMTextareaInitWrapperState(domElement, rawProps);
1013
// We listen to this event in case to ensure emulated bubble
1014
// listeners still fire for the invalid event.
1015
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
+ }
1021
break;
1022
}
1023
@@ -1059,7 +1084,9 @@ export function diffHydratedProperties(
1084
if (__DEV__ && typeof nextProp !== 'function') {
1085
warnForInvalidEventListener(propKey, nextProp);
1086
}
1062
- ensureListeningTo(rootContainerElement, propKey, domElement);
1087
+ if (!enableEagerRootListeners) {
1088
+ ensureListeningTo(rootContainerElement, propKey, domElement);
1089
+ }
1090
}
1091
} else if (
1092
__DEV__ &&
packages/react-dom/src/client/ReactDOMEventHandle.js
+22
@@ -15,6 +15,7 @@ import type {
15
} from '../shared/ReactDOMTypes';
16
17
import {getEventPriorityForListenerSystem} from '../events/DOMEventProperties';
18
+import {allNativeEvents} from '../events/EventRegistry';
19
import {
20
getClosestInstanceFromNode,
21
getEventHandlerListeners,
@@ -35,6 +36,7 @@ import {IS_EVENT_HANDLE_NON_MANAGED_NODE} from '../events/EventSystemFlags';
36
import {
37
enableScopeAPI,
38
enableCreateEventHandleAPI,
39
+ enableEagerRootListeners,
40
} from 'shared/ReactFeatureFlags';
41
import invariant from 'shared/invariant';
42
@@ -178,6 +180,26 @@ export function createEventHandle(
180
): ReactDOMEventHandle {
181
if (enableCreateEventHandleAPI) {
182
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
+
203
let isCapturePhaseListener = false;
204
let isPassiveListener = undefined; // Undefined means to use the browser default
205
let listenerPriority;
packages/react-dom/src/client/ReactDOMHostConfig.js
+10
-2
@@ -67,9 +67,13 @@ import {
67
enableFundamentalAPI,
68
enableCreateEventHandleAPI,
69
enableScopeAPI,
70
+ enableEagerRootListeners,
71
} from 'shared/ReactFeatureFlags';
72
import {HostComponent, HostText} from 'react-reconciler/src/ReactWorkTags';
72
-import {listenToReactEvent} from '../events/DOMPluginEventSystem';
73
+import {
74
+ listenToReactEvent,
75
+ listenToAllSupportedEvents,
76
+} from '../events/DOMPluginEventSystem';
77
78
export type Type = string;
79
export type Props = {
@@ -1069,7 +1073,11 @@ export function makeOpaqueHydratingObject(
1073
}
1074
1075
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
+ }
1081
}
1082
1083
export function prepareScopeUpdate(
packages/react-dom/src/client/ReactDOMRoot.js
+23
-13
@@ -35,6 +35,7 @@ import {
35
markContainerAsRoot,
36
unmarkContainerAsRoot,
37
} from './ReactDOMComponentTree';
38
+import {listenToAllSupportedEvents} from '../events/DOMPluginEventSystem';
39
import {eagerlyTrapReplayableEvents} from '../events/ReactDOMEventReplaying';
40
import {
41
ELEMENT_NODE,
@@ -51,6 +52,7 @@ import {
52
registerMutableSourceForHydration,
53
} from 'react-reconciler/src/ReactFiberReconciler';
54
import invariant from 'shared/invariant';
55
+import {enableEagerRootListeners} from 'shared/ReactFeatureFlags';
56
import {
57
BlockingRoot,
58
ConcurrentRoot,
@@ -133,19 +135,27 @@ function createRootImpl(
135
markContainerAsRoot(root.current, container);
136
const containerNodeType = container.nodeType;
137
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
+ }
159
}
160
161
if (mutableSources) {
packages/react-dom/src/events/DOMEventProperties.js
+2
-1
@@ -201,8 +201,9 @@ export function getEventPriorityForListenerSystem(
201
}
202
if (__DEV__) {
203
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.' +
205
' It is recommended to provide a "priority" option to specify a priority.',
206
+ type,
207
);
208
}
209
return ContinuousEvent;
packages/react-dom/src/events/DOMPluginEventSystem.js
+84
-41
@@ -23,7 +23,7 @@ import type {ElementListenerMapEntry} from '../client/ReactDOMComponentTree';
23
import type {EventPriority} from 'shared/ReactTypes';
24
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
25
26
-import {registrationNameDependencies} from './EventRegistry';
26
+import {registrationNameDependencies, allNativeEvents} from './EventRegistry';
27
import {
28
IS_CAPTURE_PHASE,
29
IS_EVENT_HANDLE_NON_MANAGED_NODE,
@@ -54,11 +54,13 @@ import {
54
enableCreateEventHandleAPI,
55
enableScopeAPI,
56
enablePassiveEventIntervention,
57
+ enableEagerRootListeners,
58
} from 'shared/ReactFeatureFlags';
59
import {
60
invokeGuardedCallbackAndCatchFirstError,
61
rethrowCaughtError,
62
} from 'shared/ReactErrorUtils';
63
+import {DOCUMENT_NODE} from '../shared/HTMLNodeType';
64
import {createEventListenerWrapperWithPriority} from './ReactDOMEventListener';
65
import {
66
removeEventListener,
@@ -327,6 +329,41 @@ export function listenToNonDelegatedEvent(
329
}
330
}
331
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
+
367
export function listenToNativeEvent(
368
domEventName: DOMEventName,
369
isCapturePhaseListener: boolean,
@@ -337,10 +374,14 @@ export function listenToNativeEvent(
374
eventSystemFlags?: EventSystemFlags = 0,
375
): void {
376
let target = rootContainerElement;
377
+
378
// selectionchange needs to be attached to the document
379
// otherwise it won't capture incoming events that are only
380
// triggered on the document directly.
343
- if (domEventName === 'selectionchange') {
381
+ if (
382
+ domEventName === 'selectionchange' &&
383
+ (rootContainerElement: any).nodeType !== DOCUMENT_NODE
384
+ ) {
385
target = (rootContainerElement: any).ownerDocument;
386
}
387
if (enablePassiveEventIntervention && isPassiveListener === undefined) {
@@ -426,48 +467,50 @@ export function listenToReactEvent(
467
rootContainerElement: Element,
468
targetElement: Element | null,
469
): 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;
481
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
+ }
496
}
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
+ );
513
}
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
- );
514
}
515
}
516
packages/react-dom/src/events/EventRegistry.js
+12
-2
@@ -9,6 +9,10 @@
9
10
import type {DOMEventName} from './DOMEventNames';
11
12
+import {enableEagerRootListeners} from 'shared/ReactFeatureFlags';
13
+
14
+export const allNativeEvents: Set<DOMEventName> = new Set();
15
+
16
/**
17
* Mapping from registration name to event name
18
*/
@@ -25,7 +29,7 @@ export const possibleRegistrationNames = __DEV__ ? {} : (null: any);
29
30
export function registerTwoPhaseEvent(
31
registrationName: string,
28
- dependencies: ?Array<DOMEventName>,
32
+ dependencies: Array<DOMEventName>,
33
): void {
34
registerDirectEvent(registrationName, dependencies);
35
registerDirectEvent(registrationName + 'Capture', dependencies);
@@ -33,7 +37,7 @@ export function registerTwoPhaseEvent(
37
38
export function registerDirectEvent(
39
registrationName: string,
36
- dependencies: ?Array<DOMEventName>,
40
+ dependencies: Array<DOMEventName>,
41
) {
42
if (__DEV__) {
43
if (registrationNameDependencies[registrationName]) {
@@ -55,4 +59,10 @@ export function registerDirectEvent(
59
possibleRegistrationNames.ondblclick = registrationName;
60
}
61
}
62
+
63
+ if (enableEagerRootListeners) {
64
+ for (let i = 0; i < dependencies.length; i++) {
65
+ allNativeEvents.add(dependencies[i]);
66
+ }
67
+ }
68
}
packages/react-dom/src/events/ReactDOMEventListener.js
+46
-28
@@ -32,6 +32,7 @@ import {
32
import {HostRoot, SuspenseComponent} from 'react-reconciler/src/ReactWorkTags';
33
import {
34
type EventSystemFlags,
35
+ IS_CAPTURE_PHASE,
36
IS_LEGACY_FB_SUPPORT_MODE,
37
} from './EventSystemFlags';
38
@@ -40,6 +41,7 @@ import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree';
41
42
import {
43
enableLegacyFBSupport,
44
+ enableEagerRootListeners,
45
decoupleUpdatePriorityFromScheduler,
46
} from 'shared/ReactFeatureFlags';
47
import {
@@ -191,7 +193,21 @@ export function dispatchEvent(
193
if (!_enabled) {
194
return;
195
}
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
+ ) {
211
// If we already have a queue of discrete events, and this is another discrete
212
// event, then we can't dispatch it regardless of its target, since they
213
// need to dispatch in order.
@@ -214,38 +230,40 @@ export function dispatchEvent(
230
231
if (blockedOn === null) {
232
// 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
+ }
236
return;
237
}
238
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);
265
}
266
245
- // We need to clear only if we didn't queue because
246
- // queueing is accummulative.
247
- clearIfContinuousEvent(domEventName, nativeEvent);
248
-
267
// This is not replayable so we'll invoke it but without a target,
268
// in case the event system needs to trace it.
269
dispatchEventForPluginEventSystem(
packages/react-dom/src/events/ReactDOMEventReplaying.js
+19
-10
@@ -14,7 +14,10 @@ import type {EventSystemFlags} from './EventSystemFlags';
14
import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
15
import type {LanePriority} from 'react-reconciler/src/ReactFiberLane';
16
17
-import {enableSelectiveHydration} from 'shared/ReactFeatureFlags';
17
+import {
18
+ enableSelectiveHydration,
19
+ enableEagerRootListeners,
20
+} from 'shared/ReactFeatureFlags';
21
import {
22
unstable_runWithPriority as runWithPriority,
23
unstable_scheduleCallback as scheduleCallback,
@@ -177,21 +180,27 @@ function trapReplayableEventForContainer(
180
domEventName: DOMEventName,
181
container: Container,
182
) {
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
+ }
187
}
188
189
export function eagerlyTrapReplayableEvents(
190
container: Container,
191
document: Document,
192
) {
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
+ }
204
}
205
206
function createQueuedReplayableEvent(
packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js
+138
-68
@@ -2401,85 +2401,97 @@ describe('DOMPluginEventSystem', () => {
2401
);
2402
2403
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
+ );
2426
2405
- // Test that we get a warning when we don't provide an explicit priority
2406
- expect(() => {
2427
setCustomEventHandle = ReactDOM.unstable_createEventHandle(
2428
'custom-event',
2429
+ {
2430
+ priority: 0, // Discrete
2431
+ },
2432
);
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
- );
2433
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
+ );
2441
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
+ );
2460
2450
- return () => {
2451
- clearCustom1();
2452
- clearCustom2();
2453
- clearCustom3();
2454
- clearCustom4();
2455
- };
2456
- });
2461
+ return () => {
2462
+ clearCustom1();
2463
+ clearCustom2();
2464
+ clearCustom3();
2465
+ clearCustom4();
2466
+ };
2467
+ });
2468
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
+ };
2475
2465
- ReactDOM.render(<Test />, container);
2466
- Scheduler.unstable_flushAll();
2476
+ ReactDOM.render(<Test />, container);
2477
+ Scheduler.unstable_flushAll();
2478
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]);
2485
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
+ }
2495
});
2496
2497
// @gate experimental
@@ -2823,6 +2835,64 @@ describe('DOMPluginEventSystem', () => {
2835
expect(log[5]).toEqual(['bubble', buttonElement]);
2836
});
2837
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
+
2896
describe('Compatibility with Scopes API', () => {
2897
beforeEach(() => {
2898
jest.resetModules();
packages/react-dom/src/events/plugins/SelectEventPlugin.js
+16
-13
@@ -16,6 +16,7 @@ import {canUseDOM} from 'shared/ExecutionEnvironment';
16
import {SyntheticEvent} from '../../events/SyntheticEvent';
17
import isTextInputElement from '../isTextInputElement';
18
import shallowEqual from 'shared/shallowEqual';
19
+import {enableEagerRootListeners} from 'shared/ReactFeatureFlags';
20
21
import {registerTwoPhaseEvent} from '../EventRegistry';
22
import getActiveElement from '../../client/getActiveElement';
@@ -152,19 +153,21 @@ function extractEvents(
153
eventSystemFlags: EventSystemFlags,
154
targetContainer: EventTarget,
155
) {
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
+ }
171
}
172
173
const targetNode = targetInst ? getNodeFromInstance(targetInst) : window;
packages/react-dom/src/events/plugins/__tests__/SimpleEventPlugin-test.js
+12
-1
@@ -538,7 +538,18 @@ describe('SimpleEventPlugin', function() {
538
);
539
540
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
+ }
553
} else {
554
expect(passiveEvents).toEqual([]);
555
}
packages/shared/ReactFeatureFlags.js
+2
@@ -136,3 +136,5 @@ export const enableDiscreteEventFlushingChange = false;
136
// https://github.com/facebook/react/pull/19654
137
export const enablePassiveEventIntervention = true;
138
export const disableOnScrollBubbling = true;
139
+
140
+export const enableEagerRootListeners = false;
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -51,6 +51,7 @@ export const deferRenderPhaseUpdateToNextBatch = true;
51
export const decoupleUpdatePriorityFromScheduler = false;
52
export const enableDiscreteEventFlushingChange = false;
53
export const enablePassiveEventIntervention = true;
54
+export const enableEagerRootListeners = false;
55
56
// Flow magic to verify the exports of this file match the original version.
57
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -50,6 +50,7 @@ export const deferRenderPhaseUpdateToNextBatch = true;
50
export const decoupleUpdatePriorityFromScheduler = false;
51
export const enableDiscreteEventFlushingChange = false;
52
export const enablePassiveEventIntervention = true;
53
+export const enableEagerRootListeners = false;
54
55
// Flow magic to verify the exports of this file match the original version.
56
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -50,6 +50,7 @@ export const deferRenderPhaseUpdateToNextBatch = true;
50
export const decoupleUpdatePriorityFromScheduler = false;
51
export const enableDiscreteEventFlushingChange = false;
52
export const enablePassiveEventIntervention = true;
53
+export const enableEagerRootListeners = false;
54
55
// Flow magic to verify the exports of this file match the original version.
56
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+1
@@ -49,6 +49,7 @@ export const deferRenderPhaseUpdateToNextBatch = true;
49
export const decoupleUpdatePriorityFromScheduler = false;
50
export const enableDiscreteEventFlushingChange = false;
51
export const enablePassiveEventIntervention = true;
52
+export const enableEagerRootListeners = false;
53
54
// Flow magic to verify the exports of this file match the original version.
55
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -50,6 +50,7 @@ export const deferRenderPhaseUpdateToNextBatch = true;
50
export const decoupleUpdatePriorityFromScheduler = false;
51
export const enableDiscreteEventFlushingChange = false;
52
export const enablePassiveEventIntervention = true;
53
+export const enableEagerRootListeners = false;
54
55
// Flow magic to verify the exports of this file match the original version.
56
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.testing.js
+1
@@ -50,6 +50,7 @@ export const deferRenderPhaseUpdateToNextBatch = true;
50
export const decoupleUpdatePriorityFromScheduler = false;
51
export const enableDiscreteEventFlushingChange = false;
52
export const enablePassiveEventIntervention = true;
53
+export const enableEagerRootListeners = false;
54
55
// Flow magic to verify the exports of this file match the original version.
56
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.testing.www.js
+1
@@ -50,6 +50,7 @@ export const deferRenderPhaseUpdateToNextBatch = true;
50
export const decoupleUpdatePriorityFromScheduler = false;
51
export const enableDiscreteEventFlushingChange = true;
52
export const enablePassiveEventIntervention = true;
53
+export const enableEagerRootListeners = false;
54
55
// Flow magic to verify the exports of this file match the original version.
56
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.www-dynamic.js
+1
@@ -21,6 +21,7 @@ export const decoupleUpdatePriorityFromScheduler = __VARIANT__;
21
export const skipUnmountedBoundaries = __VARIANT__;
22
export const enablePassiveEventIntervention = __VARIANT__;
23
export const disableOnScrollBubbling = __VARIANT__;
24
+export const enableEagerRootListeners = __VARIANT__;
25
26
// Enable this flag to help with concurrent mode debugging.
27
// It logs information to the console about React scheduling, rendering, and commit phases.
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -29,6 +29,7 @@ export const {
29
skipUnmountedBoundaries,
30
enablePassiveEventIntervention,
31
disableOnScrollBubbling,
32
+ enableEagerRootListeners,
33
} = dynamicFeatureFlags;
34
35
// On WWW, __EXPERIMENTAL__ is used for a new modern build.
scripts/error-codes/codes.json
+2
-1
@@ -360,5 +360,6 @@
360
"368": "ReactDOM.createEventHandle: setListener called on an invalid target. Provide a valid EventTarget or an element managed by React.",
361
"369": "ReactDOM.createEventHandle: setter called on an invalid target. Provide a valid EventTarget or an element managed by React.",
362
"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."
365
}