@samitouri / QOS-React / commits / 848bb2426e

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 }