Skip to content

Commit

Permalink
Attach Listeners Eagerly to Roots and Portal Containers
Browse files Browse the repository at this point in the history
  • Loading branch information
gaearon committed Aug 21, 2020
1 parent 2bffbef commit 2bf6238
Show file tree
Hide file tree
Showing 21 changed files with 334 additions and 164 deletions.
49 changes: 40 additions & 9 deletions packages/react-dom/src/__tests__/ReactDOMEventListener-test.js
Expand Up @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion packages/react-dom/src/__tests__/ReactDOMFiber-test.js
Expand Up @@ -1040,6 +1040,7 @@ describe('ReactDOMFiber', () => {
expect(ops).toEqual([]);
});

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

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

expect(onClick).toHaveBeenCalledTimes(1); // 0
expect(onClick).toHaveBeenCalledTimes(1);
});

it('should throw on bad createPortal argument', () => {
Expand Down
115 changes: 71 additions & 44 deletions packages/react-dom/src/client/ReactDOMComponent.js
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -587,19 +598,23 @@ 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);
props = ReactDOMTextareaGetHostProps(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;
default:
props = rawProps;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -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;
}

Expand Down Expand Up @@ -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__ &&
Expand Down
12 changes: 10 additions & 2 deletions packages/react-dom/src/client/ReactDOMHostConfig.js
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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(
Expand Down
36 changes: 23 additions & 13 deletions packages/react-dom/src/client/ReactDOMRoot.js
Expand Up @@ -35,6 +35,7 @@ import {
markContainerAsRoot,
unmarkContainerAsRoot,
} from './ReactDOMComponentTree';
import {listenToAllSupportedEvents} from '../events/DOMPluginEventSystem';
import {eagerlyTrapReplayableEvents} from '../events/ReactDOMEventReplaying';
import {
ELEMENT_NODE,
Expand All @@ -51,6 +52,7 @@ import {
registerMutableSourceForHydration,
} from 'react-reconciler/src/ReactFiberReconciler';
import invariant from 'shared/invariant';
import {enableEagerRootListeners} from 'shared/ReactFeatureFlags';
import {
BlockingRoot,
ConcurrentRoot,
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit 2bf6238

Please sign in to comment.