diff --git a/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js b/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js
index 5cb207409da73..bced36b0ff544 100644
--- a/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js
@@ -398,18 +398,49 @@ describe('ReactDOMEventListener', () => {
const originalDocAddEventListener = document.addEventListener;
const originalRootAddEventListener = container.addEventListener;
document.addEventListener = function(type) {
- throw new Error(
- `Did not expect to add a document-level listener for the "${type}" event.`,
- );
+ switch (type) {
+ case 'selectionchange':
+ break;
+ default:
+ throw new Error(
+ `Did not expect to add a document-level listener for the "${type}" event.`,
+ );
+ }
};
- container.addEventListener = function(type) {
- if (type === 'mouseout' || type === 'mouseover') {
- // We currently listen to it unconditionally.
+ container.addEventListener = function(type, fn, options) {
+ if (options && (options === true || options.capture)) {
return;
}
- throw new Error(
- `Did not expect to add a root-level listener for the "${type}" event.`,
- );
+ switch (type) {
+ case 'abort':
+ case 'canplay':
+ case 'canplaythrough':
+ case 'durationchange':
+ case 'emptied':
+ case 'encrypted':
+ case 'ended':
+ case 'error':
+ case 'loadeddata':
+ case 'loadedmetadata':
+ case 'loadstart':
+ case 'pause':
+ case 'play':
+ case 'playing':
+ case 'progress':
+ case 'ratechange':
+ case 'seeked':
+ case 'seeking':
+ case 'stalled':
+ case 'suspend':
+ case 'timeupdate':
+ case 'volumechange':
+ case 'waiting':
+ throw new Error(
+ `Did not expect to add a root-level listener for the "${type}" event.`,
+ );
+ default:
+ break;
+ }
};
try {
diff --git a/packages/react-dom/src/__tests__/ReactDOMFiber-test.js b/packages/react-dom/src/__tests__/ReactDOMFiber-test.js
index b05b4c1f96144..a91c2fac41cf3 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFiber-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFiber-test.js
@@ -1040,6 +1040,25 @@ describe('ReactDOMFiber', () => {
expect(ops).toEqual([]);
});
+ // @gate enableEagerRootListeners
+ it('listens to events that do not exist in the Portal subtree', () => {
+ const onClick = jest.fn();
+
+ const ref = React.createRef();
+ ReactDOM.render(
+
, null);
diff --git a/packages/react-dom/src/client/ReactDOMComponent.js b/packages/react-dom/src/client/ReactDOMComponent.js
index d3e72d2a2d21b..48b979af64c94 100644
--- a/packages/react-dom/src/client/ReactDOMComponent.js
+++ b/packages/react-dom/src/client/ReactDOMComponent.js
@@ -74,7 +74,10 @@ import {validateProperties as validateInputProperties} from '../shared/ReactDOMN
import {validateProperties as validateUnknownProperties} from '../shared/ReactDOMUnknownPropertyHook';
import {REACT_OPAQUE_ID_TYPE} from 'shared/ReactSymbols';
-import {enableTrustedTypesIntegration} from 'shared/ReactFeatureFlags';
+import {
+ enableTrustedTypesIntegration,
+ enableEagerRootListeners,
+} from 'shared/ReactFeatureFlags';
import {
listenToReactEvent,
mediaEventTypes,
@@ -260,30 +263,32 @@ export function ensureListeningTo(
reactPropEvent: string,
targetElement: Element | null,
): void {
- // If we have a comment node, then use the parent node,
- // which should be an element.
- const rootContainerElement =
- rootContainerInstance.nodeType === COMMENT_NODE
- ? rootContainerInstance.parentNode
- : rootContainerInstance;
- if (__DEV__) {
- if (
- rootContainerElement == null ||
- (rootContainerElement.nodeType !== ELEMENT_NODE &&
- // This is to support rendering into a ShadowRoot:
- rootContainerElement.nodeType !== DOCUMENT_FRAGMENT_NODE)
- ) {
- console.error(
- 'ensureListeningTo(): received a container that was not an element node. ' +
- 'This is likely a bug in React. Please file an issue.',
- );
+ if (!enableEagerRootListeners) {
+ // If we have a comment node, then use the parent node,
+ // which should be an element.
+ const rootContainerElement =
+ rootContainerInstance.nodeType === COMMENT_NODE
+ ? rootContainerInstance.parentNode
+ : rootContainerInstance;
+ if (__DEV__) {
+ if (
+ rootContainerElement == null ||
+ (rootContainerElement.nodeType !== ELEMENT_NODE &&
+ // This is to support rendering into a ShadowRoot:
+ rootContainerElement.nodeType !== DOCUMENT_FRAGMENT_NODE)
+ ) {
+ console.error(
+ 'ensureListeningTo(): received a container that was not an element node. ' +
+ 'This is likely a bug in React. Please file an issue.',
+ );
+ }
}
+ listenToReactEvent(
+ reactPropEvent,
+ ((rootContainerElement: any): Element),
+ targetElement,
+ );
}
- listenToReactEvent(
- reactPropEvent,
- ((rootContainerElement: any): Element),
- targetElement,
- );
}
function getOwnerDocumentFromRootContainer(
@@ -364,7 +369,11 @@ function setInitialDOMProperties(
if (__DEV__ && typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
- ensureListeningTo(rootContainerElement, propKey, domElement);
+ if (!enableEagerRootListeners) {
+ ensureListeningTo(rootContainerElement, propKey, domElement);
+ } else if (propKey === 'onScroll') {
+ listenToNonDelegatedEvent('scroll', domElement);
+ }
}
} else if (nextProp != null) {
setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag);
@@ -573,9 +582,11 @@ export function setInitialProperties(
// We listen to this event in case to ensure emulated bubble
// listeners still fire for the invalid event.
listenToNonDelegatedEvent('invalid', domElement);
- // For controlled components we always need to ensure we're listening
- // to onChange. Even if there is no listener.
- ensureListeningTo(rootContainerElement, 'onChange', domElement);
+ if (!enableEagerRootListeners) {
+ // For controlled components we always need to ensure we're listening
+ // to onChange. Even if there is no listener.
+ ensureListeningTo(rootContainerElement, 'onChange', domElement);
+ }
break;
case 'option':
ReactDOMOptionValidateProps(domElement, rawProps);
@@ -587,9 +598,11 @@ export function setInitialProperties(
// We listen to this event in case to ensure emulated bubble
// listeners still fire for the invalid event.
listenToNonDelegatedEvent('invalid', domElement);
- // For controlled components we always need to ensure we're listening
- // to onChange. Even if there is no listener.
- ensureListeningTo(rootContainerElement, 'onChange', domElement);
+ if (!enableEagerRootListeners) {
+ // For controlled components we always need to ensure we're listening
+ // to onChange. Even if there is no listener.
+ ensureListeningTo(rootContainerElement, 'onChange', domElement);
+ }
break;
case 'textarea':
ReactDOMTextareaInitWrapperState(domElement, rawProps);
@@ -597,9 +610,11 @@ export function setInitialProperties(
// We listen to this event in case to ensure emulated bubble
// listeners still fire for the invalid event.
listenToNonDelegatedEvent('invalid', domElement);
- // For controlled components we always need to ensure we're listening
- // to onChange. Even if there is no listener.
- ensureListeningTo(rootContainerElement, 'onChange', domElement);
+ if (!enableEagerRootListeners) {
+ // For controlled components we always need to ensure we're listening
+ // to onChange. Even if there is no listener.
+ ensureListeningTo(rootContainerElement, 'onChange', domElement);
+ }
break;
default:
props = rawProps;
@@ -817,7 +832,11 @@ export function diffProperties(
if (__DEV__ && typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
- ensureListeningTo(rootContainerElement, propKey, domElement);
+ if (!enableEagerRootListeners) {
+ ensureListeningTo(rootContainerElement, propKey, domElement);
+ } else if (propKey === 'onScroll') {
+ listenToNonDelegatedEvent('scroll', domElement);
+ }
}
if (!updatePayload && lastProp !== nextProp) {
// This is a special case. If any listener updates we need to ensure
@@ -969,9 +988,11 @@ export function diffHydratedProperties(
// We listen to this event in case to ensure emulated bubble
// listeners still fire for the invalid event.
listenToNonDelegatedEvent('invalid', domElement);
- // For controlled components we always need to ensure we're listening
- // to onChange. Even if there is no listener.
- ensureListeningTo(rootContainerElement, 'onChange', domElement);
+ if (!enableEagerRootListeners) {
+ // For controlled components we always need to ensure we're listening
+ // to onChange. Even if there is no listener.
+ ensureListeningTo(rootContainerElement, 'onChange', domElement);
+ }
break;
case 'option':
ReactDOMOptionValidateProps(domElement, rawProps);
@@ -981,18 +1002,22 @@ export function diffHydratedProperties(
// We listen to this event in case to ensure emulated bubble
// listeners still fire for the invalid event.
listenToNonDelegatedEvent('invalid', domElement);
- // For controlled components we always need to ensure we're listening
- // to onChange. Even if there is no listener.
- ensureListeningTo(rootContainerElement, 'onChange', domElement);
+ if (!enableEagerRootListeners) {
+ // For controlled components we always need to ensure we're listening
+ // to onChange. Even if there is no listener.
+ ensureListeningTo(rootContainerElement, 'onChange', domElement);
+ }
break;
case 'textarea':
ReactDOMTextareaInitWrapperState(domElement, rawProps);
// We listen to this event in case to ensure emulated bubble
// listeners still fire for the invalid event.
listenToNonDelegatedEvent('invalid', domElement);
- // For controlled components we always need to ensure we're listening
- // to onChange. Even if there is no listener.
- ensureListeningTo(rootContainerElement, 'onChange', domElement);
+ if (!enableEagerRootListeners) {
+ // For controlled components we always need to ensure we're listening
+ // to onChange. Even if there is no listener.
+ ensureListeningTo(rootContainerElement, 'onChange', domElement);
+ }
break;
}
@@ -1059,7 +1084,9 @@ export function diffHydratedProperties(
if (__DEV__ && typeof nextProp !== 'function') {
warnForInvalidEventListener(propKey, nextProp);
}
- ensureListeningTo(rootContainerElement, propKey, domElement);
+ if (!enableEagerRootListeners) {
+ ensureListeningTo(rootContainerElement, propKey, domElement);
+ }
}
} else if (
__DEV__ &&
diff --git a/packages/react-dom/src/client/ReactDOMEventHandle.js b/packages/react-dom/src/client/ReactDOMEventHandle.js
index 19e69126aef0c..9e9219a1bdc16 100644
--- a/packages/react-dom/src/client/ReactDOMEventHandle.js
+++ b/packages/react-dom/src/client/ReactDOMEventHandle.js
@@ -15,6 +15,7 @@ import type {
} from '../shared/ReactDOMTypes';
import {getEventPriorityForListenerSystem} from '../events/DOMEventProperties';
+import {allNativeEvents} from '../events/EventRegistry';
import {
getClosestInstanceFromNode,
getEventHandlerListeners,
@@ -33,6 +34,7 @@ import {IS_EVENT_HANDLE_NON_MANAGED_NODE} from '../events/EventSystemFlags';
import {
enableScopeAPI,
enableCreateEventHandleAPI,
+ enableEagerRootListeners,
} from 'shared/ReactFeatureFlags';
import invariant from 'shared/invariant';
@@ -178,6 +180,26 @@ export function createEventHandle(
): ReactDOMEventHandle {
if (enableCreateEventHandleAPI) {
const domEventName = ((type: any): DOMEventName);
+
+ if (enableEagerRootListeners) {
+ // We cannot support arbitrary native events with eager root listeners
+ // because the eager strategy relies on knowing the whole list ahead of time.
+ // If we wanted to support this, we'd have to add code to keep track
+ // (or search) for all portal and root containers, and lazily add listeners
+ // to them whenever we see a previously unknown event. This seems like a lot
+ // of complexity for something we don't even have a particular use case for.
+ // Unfortunately, the downside of this invariant is that *removing* a native
+ // event from the list of known events has now become a breaking change for
+ // any code relying on the createEventHandle API.
+ invariant(
+ allNativeEvents.has(domEventName) ||
+ domEventName === 'beforeblur' ||
+ domEventName === 'afterblur',
+ 'Cannot call unstable_createEventHandle with "%s", as it is not an event known to React.',
+ domEventName,
+ );
+ }
+
let isCapturePhaseListener = false;
let isPassiveListener = undefined; // Undefined means to use the browser default
let listenerPriority;
diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js
index 22d38dea0439f..e5ce50add75a4 100644
--- a/packages/react-dom/src/client/ReactDOMHostConfig.js
+++ b/packages/react-dom/src/client/ReactDOMHostConfig.js
@@ -67,9 +67,13 @@ import {
enableFundamentalAPI,
enableCreateEventHandleAPI,
enableScopeAPI,
+ enableEagerRootListeners,
} from 'shared/ReactFeatureFlags';
import {HostComponent, HostText} from 'react-reconciler/src/ReactWorkTags';
-import {listenToReactEvent} from '../events/DOMPluginEventSystem';
+import {
+ listenToReactEvent,
+ listenToAllSupportedEvents,
+} from '../events/DOMPluginEventSystem';
export type Type = string;
export type Props = {
@@ -1069,7 +1073,11 @@ export function makeOpaqueHydratingObject(
}
export function preparePortalMount(portalInstance: Instance): void {
- listenToReactEvent('onMouseEnter', portalInstance, null);
+ if (enableEagerRootListeners) {
+ listenToAllSupportedEvents(portalInstance);
+ } else {
+ listenToReactEvent('onMouseEnter', portalInstance, null);
+ }
}
export function prepareScopeUpdate(
diff --git a/packages/react-dom/src/client/ReactDOMRoot.js b/packages/react-dom/src/client/ReactDOMRoot.js
index d9e446749c120..11ec850a4ab26 100644
--- a/packages/react-dom/src/client/ReactDOMRoot.js
+++ b/packages/react-dom/src/client/ReactDOMRoot.js
@@ -35,6 +35,7 @@ import {
markContainerAsRoot,
unmarkContainerAsRoot,
} from './ReactDOMComponentTree';
+import {listenToAllSupportedEvents} from '../events/DOMPluginEventSystem';
import {eagerlyTrapReplayableEvents} from '../events/ReactDOMEventReplaying';
import {
ELEMENT_NODE,
@@ -51,6 +52,7 @@ import {
registerMutableSourceForHydration,
} from 'react-reconciler/src/ReactFiberReconciler';
import invariant from 'shared/invariant';
+import {enableEagerRootListeners} from 'shared/ReactFeatureFlags';
import {
BlockingRoot,
ConcurrentRoot,
@@ -133,19 +135,27 @@ function createRootImpl(
markContainerAsRoot(root.current, container);
const containerNodeType = container.nodeType;
- if (hydrate && tag !== LegacyRoot) {
- const doc =
- containerNodeType === DOCUMENT_NODE ? container : container.ownerDocument;
- // We need to cast this because Flow doesn't work
- // with the hoisted containerNodeType. If we inline
- // it, then Flow doesn't complain. We intentionally
- // hoist it to reduce code-size.
- eagerlyTrapReplayableEvents(container, ((doc: any): Document));
- } else if (
- containerNodeType !== DOCUMENT_FRAGMENT_NODE &&
- containerNodeType !== DOCUMENT_NODE
- ) {
- ensureListeningTo(container, 'onMouseEnter', null);
+ if (enableEagerRootListeners) {
+ const rootContainerElement =
+ container.nodeType === COMMENT_NODE ? container.parentNode : container;
+ listenToAllSupportedEvents(rootContainerElement);
+ } else {
+ if (hydrate && tag !== LegacyRoot) {
+ const doc =
+ containerNodeType === DOCUMENT_NODE
+ ? container
+ : container.ownerDocument;
+ // We need to cast this because Flow doesn't work
+ // with the hoisted containerNodeType. If we inline
+ // it, then Flow doesn't complain. We intentionally
+ // hoist it to reduce code-size.
+ eagerlyTrapReplayableEvents(container, ((doc: any): Document));
+ } else if (
+ containerNodeType !== DOCUMENT_FRAGMENT_NODE &&
+ containerNodeType !== DOCUMENT_NODE
+ ) {
+ ensureListeningTo(container, 'onMouseEnter', null);
+ }
}
if (mutableSources) {
diff --git a/packages/react-dom/src/events/DOMEventProperties.js b/packages/react-dom/src/events/DOMEventProperties.js
index 342e653bd48a3..8c5090ecaee4b 100644
--- a/packages/react-dom/src/events/DOMEventProperties.js
+++ b/packages/react-dom/src/events/DOMEventProperties.js
@@ -201,8 +201,9 @@ export function getEventPriorityForListenerSystem(
}
if (__DEV__) {
console.warn(
- 'The event "type" provided to createEventHandle() does not have a known priority type.' +
+ 'The event "%s" provided to createEventHandle() does not have a known priority type.' +
' It is recommended to provide a "priority" option to specify a priority.',
+ type,
);
}
return ContinuousEvent;
diff --git a/packages/react-dom/src/events/DOMPluginEventSystem.js b/packages/react-dom/src/events/DOMPluginEventSystem.js
index 6a216ace127c4..97a37ace2e332 100644
--- a/packages/react-dom/src/events/DOMPluginEventSystem.js
+++ b/packages/react-dom/src/events/DOMPluginEventSystem.js
@@ -23,7 +23,7 @@ import type {ElementListenerMapEntry} from '../client/ReactDOMComponentTree';
import type {EventPriority} from 'shared/ReactTypes';
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
-import {registrationNameDependencies} from './EventRegistry';
+import {registrationNameDependencies, allNativeEvents} from './EventRegistry';
import {
IS_CAPTURE_PHASE,
IS_EVENT_HANDLE_NON_MANAGED_NODE,
@@ -54,11 +54,13 @@ import {
enableCreateEventHandleAPI,
enableScopeAPI,
enablePassiveEventIntervention,
+ enableEagerRootListeners,
} from 'shared/ReactFeatureFlags';
import {
invokeGuardedCallbackAndCatchFirstError,
rethrowCaughtError,
} from 'shared/ReactErrorUtils';
+import {DOCUMENT_NODE} from '../shared/HTMLNodeType';
import {createEventListenerWrapperWithPriority} from './ReactDOMEventListener';
import {
removeEventListener,
@@ -327,6 +329,41 @@ export function listenToNonDelegatedEvent(
}
}
+const listeningMarker =
+ '_reactListening' +
+ Math.random()
+ .toString(36)
+ .slice(2);
+
+export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
+ if (enableEagerRootListeners) {
+ if ((rootContainerElement: any)[listeningMarker]) {
+ // Performance optimization: don't iterate through events
+ // for the same portal container or root node more than once.
+ // TODO: once we remove the flag, we may be able to also
+ // remove some of the bookkeeping maps used for laziness.
+ return;
+ }
+ (rootContainerElement: any)[listeningMarker] = true;
+ allNativeEvents.forEach(domEventName => {
+ if (!nonDelegatedEvents.has(domEventName)) {
+ listenToNativeEvent(
+ domEventName,
+ false,
+ ((rootContainerElement: any): Element),
+ null,
+ );
+ }
+ listenToNativeEvent(
+ domEventName,
+ true,
+ ((rootContainerElement: any): Element),
+ null,
+ );
+ });
+ }
+}
+
export function listenToNativeEvent(
domEventName: DOMEventName,
isCapturePhaseListener: boolean,
@@ -337,10 +374,14 @@ export function listenToNativeEvent(
eventSystemFlags?: EventSystemFlags = 0,
): void {
let target = rootContainerElement;
+
// selectionchange needs to be attached to the document
// otherwise it won't capture incoming events that are only
// triggered on the document directly.
- if (domEventName === 'selectionchange') {
+ if (
+ domEventName === 'selectionchange' &&
+ (rootContainerElement: any).nodeType !== DOCUMENT_NODE
+ ) {
target = (rootContainerElement: any).ownerDocument;
}
if (enablePassiveEventIntervention && isPassiveListener === undefined) {
@@ -426,48 +467,50 @@ export function listenToReactEvent(
rootContainerElement: Element,
targetElement: Element | null,
): void {
- const dependencies = registrationNameDependencies[reactEvent];
- const dependenciesLength = dependencies.length;
- // If the dependencies length is 1, that means we're not using a polyfill
- // plugin like ChangeEventPlugin, BeforeInputPlugin, EnterLeavePlugin
- // and SelectEventPlugin. We always use the native bubble event phase for
- // these plugins and emulate two phase event dispatching. SimpleEventPlugin
- // always only has a single dependency and SimpleEventPlugin events also
- // use either the native capture event phase or bubble event phase, there
- // is no emulation (except for focus/blur, but that will be removed soon).
- const isPolyfillEventPlugin = dependenciesLength !== 1;
+ if (!enableEagerRootListeners) {
+ const dependencies = registrationNameDependencies[reactEvent];
+ const dependenciesLength = dependencies.length;
+ // If the dependencies length is 1, that means we're not using a polyfill
+ // plugin like ChangeEventPlugin, BeforeInputPlugin, EnterLeavePlugin
+ // and SelectEventPlugin. We always use the native bubble event phase for
+ // these plugins and emulate two phase event dispatching. SimpleEventPlugin
+ // always only has a single dependency and SimpleEventPlugin events also
+ // use either the native capture event phase or bubble event phase, there
+ // is no emulation (except for focus/blur, but that will be removed soon).
+ const isPolyfillEventPlugin = dependenciesLength !== 1;
- if (isPolyfillEventPlugin) {
- const listenerMap = getEventListenerMap(rootContainerElement);
- // For optimization, we register plugins on the listener map, so we
- // don't need to check each of their dependencies each time.
- if (!listenerMap.has(reactEvent)) {
- listenerMap.set(reactEvent, null);
- for (let i = 0; i < dependenciesLength; i++) {
- listenToNativeEvent(
- dependencies[i],
- false,
- rootContainerElement,
- targetElement,
- );
+ if (isPolyfillEventPlugin) {
+ const listenerMap = getEventListenerMap(rootContainerElement);
+ // For optimization, we register plugins on the listener map, so we
+ // don't need to check each of their dependencies each time.
+ if (!listenerMap.has(reactEvent)) {
+ listenerMap.set(reactEvent, null);
+ for (let i = 0; i < dependenciesLength; i++) {
+ listenToNativeEvent(
+ dependencies[i],
+ false,
+ rootContainerElement,
+ targetElement,
+ );
+ }
}
+ } else {
+ const isCapturePhaseListener =
+ reactEvent.substr(-7) === 'Capture' &&
+ // Edge case: onGotPointerCapture and onLostPointerCapture
+ // end with "Capture" but that's part of their event names.
+ // The Capture versions would end with CaptureCapture.
+ // So we have to check against that.
+ // This check works because none of the events we support
+ // end with "Pointer".
+ reactEvent.substr(-14, 7) !== 'Pointer';
+ listenToNativeEvent(
+ dependencies[0],
+ isCapturePhaseListener,
+ rootContainerElement,
+ targetElement,
+ );
}
- } else {
- const isCapturePhaseListener =
- reactEvent.substr(-7) === 'Capture' &&
- // Edge case: onGotPointerCapture and onLostPointerCapture
- // end with "Capture" but that's part of their event names.
- // The Capture versions would end with CaptureCapture.
- // So we have to check against that.
- // This check works because none of the events we support
- // end with "Pointer".
- reactEvent.substr(-14, 7) !== 'Pointer';
- listenToNativeEvent(
- dependencies[0],
- isCapturePhaseListener,
- rootContainerElement,
- targetElement,
- );
}
}
diff --git a/packages/react-dom/src/events/EventRegistry.js b/packages/react-dom/src/events/EventRegistry.js
index 3dd665623f42e..dfe938d113055 100644
--- a/packages/react-dom/src/events/EventRegistry.js
+++ b/packages/react-dom/src/events/EventRegistry.js
@@ -9,6 +9,10 @@
import type {DOMEventName} from './DOMEventNames';
+import {enableEagerRootListeners} from 'shared/ReactFeatureFlags';
+
+export const allNativeEvents: Set = new Set();
+
/**
* Mapping from registration name to event name
*/
@@ -25,7 +29,7 @@ export const possibleRegistrationNames = __DEV__ ? {} : (null: any);
export function registerTwoPhaseEvent(
registrationName: string,
- dependencies: ?Array,
+ dependencies: Array,
): void {
registerDirectEvent(registrationName, dependencies);
registerDirectEvent(registrationName + 'Capture', dependencies);
@@ -33,7 +37,7 @@ export function registerTwoPhaseEvent(
export function registerDirectEvent(
registrationName: string,
- dependencies: ?Array,
+ dependencies: Array,
) {
if (__DEV__) {
if (registrationNameDependencies[registrationName]) {
@@ -55,4 +59,10 @@ export function registerDirectEvent(
possibleRegistrationNames.ondblclick = registrationName;
}
}
+
+ if (enableEagerRootListeners) {
+ for (let i = 0; i < dependencies.length; i++) {
+ allNativeEvents.add(dependencies[i]);
+ }
+ }
}
diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js
index d92c6d5eb582a..d8ff5300aaf4b 100644
--- a/packages/react-dom/src/events/ReactDOMEventListener.js
+++ b/packages/react-dom/src/events/ReactDOMEventListener.js
@@ -32,6 +32,7 @@ import {
import {HostRoot, SuspenseComponent} from 'react-reconciler/src/ReactWorkTags';
import {
type EventSystemFlags,
+ IS_CAPTURE_PHASE,
IS_LEGACY_FB_SUPPORT_MODE,
} from './EventSystemFlags';
@@ -40,6 +41,7 @@ import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree';
import {
enableLegacyFBSupport,
+ enableEagerRootListeners,
decoupleUpdatePriorityFromScheduler,
} from 'shared/ReactFeatureFlags';
import {
@@ -191,7 +193,21 @@ export function dispatchEvent(
if (!_enabled) {
return;
}
- if (hasQueuedDiscreteEvents() && isReplayableDiscreteEvent(domEventName)) {
+ let allowReplay = true;
+ if (enableEagerRootListeners) {
+ // TODO: replaying capture phase events is currently broken
+ // because we used to do it during top-level native bubble handlers
+ // but now we use different bubble and capture handlers.
+ // In eager mode, we attach capture listeners early, so we need
+ // to filter them out until we fix the logic to handle them correctly.
+ // This could've been outside the flag but I put it inside to reduce risk.
+ allowReplay = (eventSystemFlags & IS_CAPTURE_PHASE) === 0;
+ }
+ if (
+ allowReplay &&
+ hasQueuedDiscreteEvents() &&
+ isReplayableDiscreteEvent(domEventName)
+ ) {
// If we already have a queue of discrete events, and this is another discrete
// event, then we can't dispatch it regardless of its target, since they
// need to dispatch in order.
@@ -214,38 +230,40 @@ export function dispatchEvent(
if (blockedOn === null) {
// We successfully dispatched this event.
- clearIfContinuousEvent(domEventName, nativeEvent);
- return;
- }
-
- if (isReplayableDiscreteEvent(domEventName)) {
- // This this to be replayed later once the target is available.
- queueDiscreteEvent(
- blockedOn,
- domEventName,
- eventSystemFlags,
- targetContainer,
- nativeEvent,
- );
+ if (allowReplay) {
+ clearIfContinuousEvent(domEventName, nativeEvent);
+ }
return;
}
- if (
- queueIfContinuousEvent(
- blockedOn,
- domEventName,
- eventSystemFlags,
- targetContainer,
- nativeEvent,
- )
- ) {
- return;
+ if (allowReplay) {
+ if (isReplayableDiscreteEvent(domEventName)) {
+ // This this to be replayed later once the target is available.
+ queueDiscreteEvent(
+ blockedOn,
+ domEventName,
+ eventSystemFlags,
+ targetContainer,
+ nativeEvent,
+ );
+ return;
+ }
+ if (
+ queueIfContinuousEvent(
+ blockedOn,
+ domEventName,
+ eventSystemFlags,
+ targetContainer,
+ nativeEvent,
+ )
+ ) {
+ return;
+ }
+ // We need to clear only if we didn't queue because
+ // queueing is accummulative.
+ clearIfContinuousEvent(domEventName, nativeEvent);
}
- // We need to clear only if we didn't queue because
- // queueing is accummulative.
- clearIfContinuousEvent(domEventName, nativeEvent);
-
// This is not replayable so we'll invoke it but without a target,
// in case the event system needs to trace it.
dispatchEventForPluginEventSystem(
diff --git a/packages/react-dom/src/events/ReactDOMEventReplaying.js b/packages/react-dom/src/events/ReactDOMEventReplaying.js
index ded99d196188b..8b8d08f5e06a8 100644
--- a/packages/react-dom/src/events/ReactDOMEventReplaying.js
+++ b/packages/react-dom/src/events/ReactDOMEventReplaying.js
@@ -14,7 +14,10 @@ import type {EventSystemFlags} from './EventSystemFlags';
import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
import type {LanePriority} from 'react-reconciler/src/ReactFiberLane';
-import {enableSelectiveHydration} from 'shared/ReactFeatureFlags';
+import {
+ enableSelectiveHydration,
+ enableEagerRootListeners,
+} from 'shared/ReactFeatureFlags';
import {
unstable_runWithPriority as runWithPriority,
unstable_scheduleCallback as scheduleCallback,
@@ -177,21 +180,27 @@ function trapReplayableEventForContainer(
domEventName: DOMEventName,
container: Container,
) {
- listenToNativeEvent(domEventName, false, ((container: any): Element), null);
+ // When the flag is on, we do this in a unified codepath elsewhere.
+ if (!enableEagerRootListeners) {
+ listenToNativeEvent(domEventName, false, ((container: any): Element), null);
+ }
}
export function eagerlyTrapReplayableEvents(
container: Container,
document: Document,
) {
- // Discrete
- discreteReplayableEvents.forEach(domEventName => {
- trapReplayableEventForContainer(domEventName, container);
- });
- // Continuous
- continuousReplayableEvents.forEach(domEventName => {
- trapReplayableEventForContainer(domEventName, container);
- });
+ // When the flag is on, we do this in a unified codepath elsewhere.
+ if (!enableEagerRootListeners) {
+ // Discrete
+ discreteReplayableEvents.forEach(domEventName => {
+ trapReplayableEventForContainer(domEventName, container);
+ });
+ // Continuous
+ continuousReplayableEvents.forEach(domEventName => {
+ trapReplayableEventForContainer(domEventName, container);
+ });
+ }
}
function createQueuedReplayableEvent(
diff --git a/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js
index d0e5ba484573d..17d624d8e1f05 100644
--- a/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js
+++ b/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js
@@ -2401,85 +2401,97 @@ describe('DOMPluginEventSystem', () => {
);
let setCustomEventHandle;
+ if (gate(flags => flags.enableEagerRootListeners)) {
+ // With eager listeners, supporting custom events via this API doesn't make sense
+ // because we can't know a full list of them ahead of time. Let's check we throw
+ // since otherwise we'd end up with inconsistent behavior, like no portal bubbling.
+ expect(() => {
+ setCustomEventHandle = ReactDOM.unstable_createEventHandle(
+ 'custom-event',
+ );
+ }).toThrow(
+ 'Cannot call unstable_createEventHandle with "custom-event", as it is not an event known to React.',
+ );
+ } else {
+ // Test that we get a warning when we don't provide an explicit priority
+ expect(() => {
+ setCustomEventHandle = ReactDOM.unstable_createEventHandle(
+ 'custom-event',
+ );
+ }).toWarnDev(
+ 'Warning: The event "custom-event" provided to createEventHandle() does not have a known priority type. ' +
+ 'It is recommended to provide a "priority" option to specify a priority.',
+ {withoutStack: true},
+ );
- // Test that we get a warning when we don't provide an explicit priority
- expect(() => {
setCustomEventHandle = ReactDOM.unstable_createEventHandle(
'custom-event',
+ {
+ priority: 0, // Discrete
+ },
);
- }).toWarnDev(
- 'Warning: The event "type" provided to createEventHandle() does not have a known priority type. ' +
- 'It is recommended to provide a "priority" option to specify a priority.',
- {withoutStack: true},
- );
- setCustomEventHandle = ReactDOM.unstable_createEventHandle(
- 'custom-event',
- {
- priority: 0, // Discrete
- },
- );
-
- const setCustomCaptureHandle = ReactDOM.unstable_createEventHandle(
- 'custom-event',
- {
- capture: true,
- priority: 0, // Discrete
- },
- );
+ const setCustomCaptureHandle = ReactDOM.unstable_createEventHandle(
+ 'custom-event',
+ {
+ capture: true,
+ priority: 0, // Discrete
+ },
+ );
- function Test() {
- React.useEffect(() => {
- const clearCustom1 = setCustomEventHandle(
- buttonRef.current,
- onCustomEvent,
- );
- const clearCustom2 = setCustomCaptureHandle(
- buttonRef.current,
- onCustomEventCapture,
- );
- const clearCustom3 = setCustomEventHandle(
- divRef.current,
- onCustomEvent,
- );
- const clearCustom4 = setCustomCaptureHandle(
- divRef.current,
- onCustomEventCapture,
- );
+ const Test = () => {
+ React.useEffect(() => {
+ const clearCustom1 = setCustomEventHandle(
+ buttonRef.current,
+ onCustomEvent,
+ );
+ const clearCustom2 = setCustomCaptureHandle(
+ buttonRef.current,
+ onCustomEventCapture,
+ );
+ const clearCustom3 = setCustomEventHandle(
+ divRef.current,
+ onCustomEvent,
+ );
+ const clearCustom4 = setCustomCaptureHandle(
+ divRef.current,
+ onCustomEventCapture,
+ );
- return () => {
- clearCustom1();
- clearCustom2();
- clearCustom3();
- clearCustom4();
- };
- });
+ return () => {
+ clearCustom1();
+ clearCustom2();
+ clearCustom3();
+ clearCustom4();
+ };
+ });
- return (
-
- );
- }
+ return (
+
+ );
+ };
- ReactDOM.render(, container);
- Scheduler.unstable_flushAll();
+ ReactDOM.render(, container);
+ Scheduler.unstable_flushAll();
- const buttonElement = buttonRef.current;
- dispatchEvent(buttonElement, 'custom-event');
- expect(onCustomEvent).toHaveBeenCalledTimes(1);
- expect(onCustomEventCapture).toHaveBeenCalledTimes(1);
- expect(log[0]).toEqual(['capture', buttonElement]);
- expect(log[1]).toEqual(['bubble', buttonElement]);
+ const buttonElement = buttonRef.current;
+ dispatchEvent(buttonElement, 'custom-event');
+ expect(onCustomEvent).toHaveBeenCalledTimes(1);
+ expect(onCustomEventCapture).toHaveBeenCalledTimes(1);
+ expect(log[0]).toEqual(['capture', buttonElement]);
+ expect(log[1]).toEqual(['bubble', buttonElement]);
- const divElement = divRef.current;
- dispatchEvent(divElement, 'custom-event');
- expect(onCustomEvent).toHaveBeenCalledTimes(3);
- expect(onCustomEventCapture).toHaveBeenCalledTimes(3);
- expect(log[2]).toEqual(['capture', buttonElement]);
- expect(log[3]).toEqual(['capture', divElement]);
- expect(log[4]).toEqual(['bubble', divElement]);
- expect(log[5]).toEqual(['bubble', buttonElement]);
+ const divElement = divRef.current;
+ dispatchEvent(divElement, 'custom-event');
+ expect(onCustomEvent).toHaveBeenCalledTimes(3);
+ expect(onCustomEventCapture).toHaveBeenCalledTimes(3);
+ expect(log[2]).toEqual(['capture', buttonElement]);
+ expect(log[3]).toEqual(['capture', divElement]);
+ expect(log[4]).toEqual(['bubble', divElement]);
+ expect(log[5]).toEqual(['bubble', buttonElement]);
+ }
});
// @gate experimental
@@ -2823,6 +2835,64 @@ describe('DOMPluginEventSystem', () => {
expect(log[5]).toEqual(['bubble', buttonElement]);
});
+ // @gate experimental && enableEagerRootListeners
+ it('propagates known createEventHandle events through portals without inner listeners', () => {
+ const buttonRef = React.createRef();
+ const divRef = React.createRef();
+ const log = [];
+ const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
+ const onClickCapture = jest.fn(e =>
+ log.push(['capture', e.currentTarget]),
+ );
+ const setClick = ReactDOM.unstable_createEventHandle('click');
+ const setClickCapture = ReactDOM.unstable_createEventHandle(
+ 'click',
+ {
+ capture: true,
+ },
+ );
+
+ const portalElement = document.createElement('div');
+ document.body.appendChild(portalElement);
+
+ function Child() {
+ return
Click me!
;
+ }
+
+ function Parent() {
+ React.useEffect(() => {
+ const clear1 = setClick(buttonRef.current, onClick);
+ const clear2 = setClickCapture(
+ buttonRef.current,
+ onClickCapture,
+ );
+ return () => {
+ clear1();
+ clear2();
+ };
+ });
+
+ return (
+
+ );
+ }
+
+ ReactDOM.render(, container);
+ Scheduler.unstable_flushAll();
+
+ const divElement = divRef.current;
+ const buttonElement = buttonRef.current;
+ dispatchClickEvent(divElement);
+ expect(onClick).toHaveBeenCalledTimes(1);
+ expect(onClickCapture).toHaveBeenCalledTimes(1);
+ expect(log[0]).toEqual(['capture', buttonElement]);
+ expect(log[1]).toEqual(['bubble', buttonElement]);
+
+ document.body.removeChild(portalElement);
+ });
+
describe('Compatibility with Scopes API', () => {
beforeEach(() => {
jest.resetModules();
diff --git a/packages/react-dom/src/events/plugins/SelectEventPlugin.js b/packages/react-dom/src/events/plugins/SelectEventPlugin.js
index bc5d7df1a4954..7f8b72408f993 100644
--- a/packages/react-dom/src/events/plugins/SelectEventPlugin.js
+++ b/packages/react-dom/src/events/plugins/SelectEventPlugin.js
@@ -16,6 +16,7 @@ import {canUseDOM} from 'shared/ExecutionEnvironment';
import {SyntheticEvent} from '../../events/SyntheticEvent';
import isTextInputElement from '../isTextInputElement';
import shallowEqual from 'shared/shallowEqual';
+import {enableEagerRootListeners} from 'shared/ReactFeatureFlags';
import {registerTwoPhaseEvent} from '../EventRegistry';
import getActiveElement from '../../client/getActiveElement';
@@ -152,19 +153,21 @@ function extractEvents(
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
) {
- const eventListenerMap = getEventListenerMap(targetContainer);
- // Track whether all listeners exists for this plugin. If none exist, we do
- // not extract events. See #3639.
- if (
- // If we are handling selectionchange, then we don't need to
- // check for the other dependencies, as selectionchange is only
- // event attached from the onChange plugin and we don't expose an
- // onSelectionChange event from React.
- domEventName !== 'selectionchange' &&
- !eventListenerMap.has('onSelect') &&
- !eventListenerMap.has('onSelectCapture')
- ) {
- return;
+ if (!enableEagerRootListeners) {
+ const eventListenerMap = getEventListenerMap(targetContainer);
+ // Track whether all listeners exists for this plugin. If none exist, we do
+ // not extract events. See #3639.
+ if (
+ // If we are handling selectionchange, then we don't need to
+ // check for the other dependencies, as selectionchange is only
+ // event attached from the onChange plugin and we don't expose an
+ // onSelectionChange event from React.
+ domEventName !== 'selectionchange' &&
+ !eventListenerMap.has('onSelect') &&
+ !eventListenerMap.has('onSelectCapture')
+ ) {
+ return;
+ }
}
const targetNode = targetInst ? getNodeFromInstance(targetInst) : window;
diff --git a/packages/react-dom/src/events/plugins/__tests__/SimpleEventPlugin-test.js b/packages/react-dom/src/events/plugins/__tests__/SimpleEventPlugin-test.js
index d8eb2c9219e8d..4614052f50b76 100644
--- a/packages/react-dom/src/events/plugins/__tests__/SimpleEventPlugin-test.js
+++ b/packages/react-dom/src/events/plugins/__tests__/SimpleEventPlugin-test.js
@@ -538,7 +538,18 @@ describe('SimpleEventPlugin', function() {
);
if (gate(flags => flags.enablePassiveEventIntervention)) {
- expect(passiveEvents).toEqual(['touchstart', 'touchmove', 'wheel']);
+ if (gate(flags => flags.enableEagerRootListeners)) {
+ expect(passiveEvents).toEqual([
+ 'touchstart',
+ 'touchstart',
+ 'touchmove',
+ 'touchmove',
+ 'wheel',
+ 'wheel',
+ ]);
+ } else {
+ expect(passiveEvents).toEqual(['touchstart', 'touchmove', 'wheel']);
+ }
} else {
expect(passiveEvents).toEqual([]);
}
diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js
index 2129245913c19..1ae1c94e618ab 100644
--- a/packages/shared/ReactFeatureFlags.js
+++ b/packages/shared/ReactFeatureFlags.js
@@ -136,3 +136,5 @@ export const enableDiscreteEventFlushingChange = false;
// https://github.com/facebook/react/pull/19654
export const enablePassiveEventIntervention = true;
export const disableOnScrollBubbling = true;
+
+export const enableEagerRootListeners = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js
index 078d1e6b0f43f..ed2b0952cb187 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-fb.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js
@@ -51,6 +51,7 @@ export const deferRenderPhaseUpdateToNextBatch = true;
export const decoupleUpdatePriorityFromScheduler = false;
export const enableDiscreteEventFlushingChange = false;
export const enablePassiveEventIntervention = true;
+export const enableEagerRootListeners = false;
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js
index 89512478b4515..0137e890879f7 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-oss.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js
@@ -50,6 +50,7 @@ export const deferRenderPhaseUpdateToNextBatch = true;
export const decoupleUpdatePriorityFromScheduler = false;
export const enableDiscreteEventFlushingChange = false;
export const enablePassiveEventIntervention = true;
+export const enableEagerRootListeners = false;
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js
index 280fc35872905..9481dfda8c797 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js
@@ -50,6 +50,7 @@ export const deferRenderPhaseUpdateToNextBatch = true;
export const decoupleUpdatePriorityFromScheduler = false;
export const enableDiscreteEventFlushingChange = false;
export const enablePassiveEventIntervention = true;
+export const enableEagerRootListeners = false;
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
index baee0a991e580..4a710543cb94a 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
@@ -49,6 +49,7 @@ export const deferRenderPhaseUpdateToNextBatch = true;
export const decoupleUpdatePriorityFromScheduler = false;
export const enableDiscreteEventFlushingChange = false;
export const enablePassiveEventIntervention = true;
+export const enableEagerRootListeners = false;
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
index aeb589d1b837c..5efc2138d0ef2 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
@@ -50,6 +50,7 @@ export const deferRenderPhaseUpdateToNextBatch = true;
export const decoupleUpdatePriorityFromScheduler = false;
export const enableDiscreteEventFlushingChange = false;
export const enablePassiveEventIntervention = true;
+export const enableEagerRootListeners = false;
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
diff --git a/packages/shared/forks/ReactFeatureFlags.testing.js b/packages/shared/forks/ReactFeatureFlags.testing.js
index 1ba4439b28e97..8a715c02f9620 100644
--- a/packages/shared/forks/ReactFeatureFlags.testing.js
+++ b/packages/shared/forks/ReactFeatureFlags.testing.js
@@ -50,6 +50,7 @@ export const deferRenderPhaseUpdateToNextBatch = true;
export const decoupleUpdatePriorityFromScheduler = false;
export const enableDiscreteEventFlushingChange = false;
export const enablePassiveEventIntervention = true;
+export const enableEagerRootListeners = false;
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
diff --git a/packages/shared/forks/ReactFeatureFlags.testing.www.js b/packages/shared/forks/ReactFeatureFlags.testing.www.js
index b1b37b557f482..3dbbb37824441 100644
--- a/packages/shared/forks/ReactFeatureFlags.testing.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.testing.www.js
@@ -50,6 +50,7 @@ export const deferRenderPhaseUpdateToNextBatch = true;
export const decoupleUpdatePriorityFromScheduler = false;
export const enableDiscreteEventFlushingChange = true;
export const enablePassiveEventIntervention = true;
+export const enableEagerRootListeners = false;
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
diff --git a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js
index f58f9f8aae2e6..3cf35c860738e 100644
--- a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js
+++ b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js
@@ -21,6 +21,7 @@ export const decoupleUpdatePriorityFromScheduler = __VARIANT__;
export const skipUnmountedBoundaries = __VARIANT__;
export const enablePassiveEventIntervention = __VARIANT__;
export const disableOnScrollBubbling = __VARIANT__;
+export const enableEagerRootListeners = __VARIANT__;
// Enable this flag to help with concurrent mode debugging.
// It logs information to the console about React scheduling, rendering, and commit phases.
diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js
index d9c237b048a6a..dbbaf0c70daab 100644
--- a/packages/shared/forks/ReactFeatureFlags.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.www.js
@@ -29,6 +29,7 @@ export const {
skipUnmountedBoundaries,
enablePassiveEventIntervention,
disableOnScrollBubbling,
+ enableEagerRootListeners,
} = dynamicFeatureFlags;
// On WWW, __EXPERIMENTAL__ is used for a new modern build.
diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json
index 9498d6e5ed3b1..06174f33af566 100644
--- a/scripts/error-codes/codes.json
+++ b/scripts/error-codes/codes.json
@@ -360,5 +360,6 @@
"368": "ReactDOM.createEventHandle: setListener called on an invalid target. Provide a valid EventTarget or an element managed by React.",
"369": "ReactDOM.createEventHandle: setter called on an invalid target. Provide a valid EventTarget or an element managed by React.",
"370": "ReactDOM.createEventHandle: setter called with an invalid callback. The callback must be a function.",
- "371": "Text string must be rendered within a component.\n\nText: %s"
+ "371": "Text string must be rendered within a component.\n\nText: %s",
+ "372": "Cannot call unstable_createEventHandle with \"%s\", as it is not an event known to React."
}