Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions packages/react-dom-bindings/src/client/ReactDOMComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -379,12 +379,18 @@ export function trapClickOnNonInteractiveElement(node: HTMLElement) {
// listener on the target node.
// https://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
// Just set it using the onclick property so that we don't have to manage any
// bookkeeping for it. Not sure if we need to clear it when the listener is
// removed.
// bookkeeping for it. HostSingleton release clears the property only if it
// still points to this noop.
// TODO: Only do this for the relevant Safaris maybe?
node.onclick = noop;
}

export function clearClickListener(node: HTMLElement) {
if (node.onclick === noop) {
node.onclick = null;
}
}

const xlinkNamespace = 'http://www.w3.org/1999/xlink';
const xmlNamespace = 'http://www.w3.org/XML/1998/namespace';

Expand Down Expand Up @@ -1489,6 +1495,26 @@ export function setInitialProperties(
}
}

export type SingletonType = 'html' | 'head' | 'body';

const emptyProps = {};

export function clearSingletonProperties(
domElement: Element,
tag: SingletonType,
props: Object,
): void {
// This is equivalent to updating to empty props for tags without
// tag-specific update logic. Host singletons are limited to html, head, and
// body, so they always use this generic path.
for (const propKey in props) {
const propValue = props[propKey];
if (props.hasOwnProperty(propKey) && propValue != null) {
setProp(domElement, tag, propKey, null, emptyProps, propValue);
}
}
}

export function updateProperties(
domElement: Element,
tag: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,12 @@ const internalPropsMap:
| Map<InstanceUnion, Props> = new PossiblyWeakMap();

export function detachDeletedInstance(node: Instance): void {
// Don't delete the event listener set. The native event listeners it tracks
// remain attached to the node, so this bookkeeping needs to last for the
// lifetime of the node to prevent duplicate listeners if it is reused.
if (enableInternalInstanceMap) {
internalInstanceMap.delete(node);
internalPropsMap.delete(node);
delete (node as any)[internalEventHandlersKey];
delete (node as any)[internalEventHandlerListenersKey];
delete (node as any)[internalEventHandlesSetKey];
delete (node as any)[internalRootNodeResourcesKey];
Expand All @@ -85,7 +87,6 @@ export function detachDeletedInstance(node: Instance): void {
// these fields are relevant.
delete (node as any)[internalInstanceKey];
delete (node as any)[internalPropsKey];
delete (node as any)[internalEventHandlersKey];
delete (node as any)[internalEventHandlerListenersKey];
delete (node as any)[internalEventHandlesSetKey];
}
Expand Down
44 changes: 40 additions & 4 deletions packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,18 @@ import {compareDocumentPositionForEmptyFragment} from 'shared/ReactDOMFragmentRe

export {detachDeletedInstance};
import {hasRole} from './DOMAccessibilityRoles';
import type {SingletonType} from './ReactDOMComponent';
import {
setInitialProperties,
updateProperties,
clearSingletonProperties,
hydrateProperties,
hydrateText,
diffHydratedProperties,
getPropsFromElement,
diffHydratedText,
trapClickOnNonInteractiveElement,
clearClickListener,
} from './ReactDOMComponent';
import {hydrateInput} from './ReactDOMInput';
import {hydrateTextarea} from './ReactDOMTextarea';
Expand Down Expand Up @@ -1269,18 +1272,18 @@ function clearHydrationBoundary(
// then it contributed to the html tag and we need to reset it.
const ownerDocument = parentInstance.ownerDocument;
const documentElement: Element = ownerDocument.documentElement as any;
releaseSingletonInstance(documentElement);
clearSingletonPreambleContribution(documentElement);
} else if (data === PREAMBLE_CONTRIBUTION_HEAD) {
const ownerDocument = parentInstance.ownerDocument;
const head: Element = ownerDocument.head as any;
releaseSingletonInstance(head);
clearSingletonPreambleContribution(head);
// We need to clear the head because this is the only singleton that can have children that
// were part of this boundary but are not inside this boundary.
clearHead(head);
} else if (data === PREAMBLE_CONTRIBUTION_BODY) {
const ownerDocument = parentInstance.ownerDocument;
const body: Element = ownerDocument.body as any;
releaseSingletonInstance(body);
clearSingletonPreambleContribution(body);
}
}
// $FlowFixMe[incompatible-type] we bail out when we get a null
Expand Down Expand Up @@ -4844,7 +4847,40 @@ export function acquireSingletonInstance(
updateFiberProps(instance, props);
}

export function releaseSingletonInstance(instance: Instance): void {
export function releaseSingletonInstance(
instance: Instance,
type: SingletonType,
props: Props,
): void {
// Remove the attributes and property-backed state owned by this Fiber.
clearSingletonProperties(instance, type, props);

// These properties aren't cleared by updateProperties when their next
// value is null. Normally that is handled by replacing/removing the host
// instance, but a singleton cannot be removed.
// TODO: HostSingleton updates do not currently schedule ContentReset when
// dangerouslySetInnerHTML becomes undefined, so an ordinary update can leave
// the previous HTML in place. This only handles the release path.
if (props.dangerouslySetInnerHTML != null) {
instance.textContent = '';
}
clearClickListener(instance as any as HTMLElement);

// Only remove state that was represented by this Fiber's props. Attributes
// added imperatively while React owned the singleton must be preserved.
detachDeletedInstance(instance);
}

function clearSingletonPreambleContribution(instance: Instance): void {
// This path is only used when clearing a dehydrated boundary that contains a
// Fizz preamble contribution marker. The marker tells us which singleton the
// boundary contributed to, but it does not include the contributed props and
// there is no HostSingleton Fiber to provide them. We therefore cannot tell
// which attributes came from React and which were added imperatively by a
// script or third party. For now, clearing every attribute is an accepted
// edge case.
// TODO: Include the contributed properties in the marker so this cleanup can
// remove only the attributes owned by the boundary.
const attributes = instance.attributes;
while (attributes.length) {
instance.removeAttributeNode(attributes[0]);
Expand Down
Loading
Loading