Skip to content

Commit c3ed935

Browse files
committed
handle multiple copies of singletons with appropriate warnings
1 parent 86ed32e commit c3ed935

File tree

6 files changed

+135
-53
lines changed

6 files changed

+135
-53
lines changed

packages/react-dom-bindings/src/client/ReactDOMHostConfig.js

Lines changed: 75 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ import {
2525
getInstanceFromNode as getInstanceFromNodeDOMTree,
2626
isContainerMarkedAsRoot,
2727
} from './ReactDOMComponentTree';
28-
export {detachDeletedInstance} from './ReactDOMComponentTree';
28+
import {detachDeletedInstance} from './ReactDOMComponentTree';
29+
export {detachDeletedInstance};
2930
import {hasRole} from './DOMAccessibilityRoles';
3031
import {
3132
createElement,
@@ -710,43 +711,55 @@ export function unhideTextInstance(
710711

711712
export function clearContainer(container: Container): void {
712713
if (enableHostSingletons) {
713-
if (container.nodeType === ELEMENT_NODE) {
714-
// We have refined the container to Element type
715-
const element: Element = (container: any);
716-
switch (element.tagName) {
714+
// We have refined the container to Element type
715+
const nodeType = container.nodeType;
716+
if (nodeType === DOCUMENT_NODE || nodeType === ELEMENT_NODE) {
717+
switch (container.nodeName) {
718+
case '#document':
717719
case 'HTML':
718720
case 'HEAD':
719721
case 'BODY': {
720-
let node = element.firstChild;
722+
let node = container.firstChild;
721723
while (node) {
722724
const nextNode = node.nextSibling;
723725
const nodeName = node.nodeName;
724-
if (getInstanceFromNodeDOMTree(node)) {
725-
// retain nodes owned by React
726-
} else if (
727-
nodeName === 'HEAD' ||
728-
nodeName === 'BODY' ||
729-
nodeName === 'STYLE' ||
730-
(nodeName === 'LINK' &&
731-
((node: any): HTMLLinkElement).rel.toLowerCase() ===
732-
'stylesheet')
733-
) {
734-
// retain these nodes
735-
} else {
736-
element.removeChild(node);
726+
switch (nodeName) {
727+
case 'HTML':
728+
case 'HEAD':
729+
case 'BODY': {
730+
clearContainer((node: any));
731+
// If these singleton instances had previously been rendered with React they
732+
// may still hold on to references to the previous fiber tree. We detatch them
733+
// prospectiveyl to reset them to a baseline starting state since we cannot create
734+
// new instances.
735+
detachDeletedInstance((node: any));
736+
break;
737+
}
738+
case 'STYLE': {
739+
break;
740+
}
741+
case 'LINK': {
742+
if (
743+
((node: any): HTMLLinkElement).rel.toLowerCase() ===
744+
'stylesheet'
745+
) {
746+
break;
747+
}
748+
}
749+
// eslint-disable-next-line no-fallthrough
750+
default: {
751+
container.removeChild(node);
752+
}
737753
}
738754
node = nextNode;
739755
}
740756
return;
741757
}
742758
default: {
743-
element.textContent = '';
759+
container.textContent = '';
744760
}
745761
}
746762
}
747-
// Implicitly if the container is of type Document we rely on the HostSingleton
748-
// semantics to clear these nodes appropriately when being placed so no ahead of time
749-
// clearing is necessary
750763
} else {
751764
if (container.nodeType === ELEMENT_NODE) {
752765
// We have refined the container to Element type
@@ -762,6 +775,19 @@ export function clearContainer(container: Container): void {
762775
}
763776
}
764777

778+
// Making this so we can eventually move all of the instance caching to the commit phase.
779+
// Currently this is only used to associate fiber and props to instances for hydrating
780+
// HostSingletons. The reason we need it here is we only want to make this binding on commit
781+
// because only one fiber can own the instance at a time and render can fail/restart
782+
export function bindInstance(
783+
instance: Instance,
784+
props: Props,
785+
internalInstanceHandle: mixed,
786+
) {
787+
precacheFiberNode((internalInstanceHandle: any), instance);
788+
updateFiberProps(instance, props);
789+
}
790+
765791
// -------------------
766792
// Hydration
767793
// -------------------
@@ -1551,7 +1577,7 @@ export function acquireSingletonInstance(
15511577
'You are mounting a new %s component when a previous one has not first unmounted. It is an' +
15521578
' error to render more than one %s component at a time and attributes and children of these' +
15531579
' components will likely fail in unpredictable ways. Please only render a single instance of' +
1554-
' <%s> and if you need to mount a new one, ensure any previous ones have unmounted first',
1580+
' <%s> and if you need to mount a new one, ensure any previous ones have unmounted first.',
15551581
tagName,
15561582
tagName,
15571583
tagName,
@@ -1586,4 +1612,29 @@ export function releaseSingletonInstance(instance: Instance): void {
15861612
while (attributes.length) {
15871613
instance.removeAttributeNode(attributes[0]);
15881614
}
1615+
detachDeletedInstance(instance);
1616+
}
1617+
1618+
export function clearSingleton(instance: Instance): void {
1619+
const element: Element = (instance: any);
1620+
let node = element.firstChild;
1621+
while (node) {
1622+
const nextNode = node.nextSibling;
1623+
const nodeName = node.nodeName;
1624+
if (getInstanceFromNodeDOMTree(node)) {
1625+
// retain nodes owned by React
1626+
} else if (
1627+
nodeName === 'HEAD' ||
1628+
nodeName === 'BODY' ||
1629+
nodeName === 'STYLE' ||
1630+
(nodeName === 'LINK' &&
1631+
((node: any): HTMLLinkElement).rel.toLowerCase() === 'stylesheet')
1632+
) {
1633+
// retain these nodes
1634+
} else {
1635+
element.removeChild(node);
1636+
}
1637+
node = nextNode;
1638+
}
1639+
return;
15891640
}

packages/react-dom/src/__tests__/validateDOMNesting-test.js

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -102,22 +102,49 @@ describe('validateDOMNesting', () => {
102102
' in html (at **)',
103103
],
104104
);
105-
expectWarnings(
106-
['body', 'body'],
107-
[
108-
'render(): Rendering components directly into document.body is discouraged',
109-
'validateDOMNesting(...): <body> cannot appear as a child of <body>.\n' +
110-
' in body (at **)',
111-
],
112-
1,
113-
);
114-
expectWarnings(
115-
['svg', 'foreignObject', 'body', 'p'],
116-
[
117-
'validateDOMNesting(...): <body> cannot appear as a child of <foreignObject>.\n' +
118-
' in body (at **)\n' +
119-
' in foreignObject (at **)',
120-
],
121-
);
105+
if (gate(flags => flags.enableHostSingletons)) {
106+
expectWarnings(
107+
['body', 'body'],
108+
[
109+
'render(): Rendering components directly into document.body is discouraged',
110+
'validateDOMNesting(...): <body> cannot appear as a child of <body>.\n' +
111+
' in body (at **)',
112+
'Warning: You are mounting a new body component when a previous one has not first unmounted. It is an error to render more than one body component at a time and attributes and children of these components will likely fail in unpredictable ways. Please only render a single instance of <body> and if you need to mount a new one, ensure any previous ones have unmounted first.\n' +
113+
' in body (at **)',
114+
],
115+
1,
116+
);
117+
} else {
118+
expectWarnings(
119+
['body', 'body'],
120+
[
121+
'render(): Rendering components directly into document.body is discouraged',
122+
'validateDOMNesting(...): <body> cannot appear as a child of <body>.\n' +
123+
' in body (at **)',
124+
],
125+
1,
126+
);
127+
}
128+
if (gate(flags => flags.enableHostSingletons)) {
129+
expectWarnings(
130+
['svg', 'foreignObject', 'body', 'p'],
131+
[
132+
'validateDOMNesting(...): <body> cannot appear as a child of <foreignObject>.\n' +
133+
' in body (at **)\n' +
134+
' in foreignObject (at **)',
135+
'Warning: You are mounting a new body component when a previous one has not first unmounted. It is an error to render more than one body component at a time and attributes and children of these components will likely fail in unpredictable ways. Please only render a single instance of <body> and if you need to mount a new one, ensure any previous ones have unmounted first.\n' +
136+
' in body (at **)',
137+
],
138+
);
139+
} else {
140+
expectWarnings(
141+
['svg', 'foreignObject', 'body', 'p'],
142+
[
143+
'validateDOMNesting(...): <body> cannot appear as a child of <foreignObject>.\n' +
144+
' in body (at **)\n' +
145+
' in foreignObject (at **)',
146+
],
147+
);
148+
}
122149
});
123150
});

packages/react-reconciler/src/ReactFiberCommitWork.new.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ import {
150150
detachDeletedInstance,
151151
acquireResource,
152152
releaseResource,
153+
clearSingleton,
153154
acquireSingletonInstance,
154155
releaseSingletonInstance,
155156
} from './ReactFiberHostConfig';
@@ -2096,7 +2097,6 @@ function commitDeletionEffectsOnFiber(
20962097
// a different fiber. To increase our chances of avoiding this, specifically
20972098
// if you keyed a HostSingleton so there will be a delete followed by a Placement
20982099
// we treat detach eagerly here
2099-
detachDeletedInstance(deletedFiber.stateNode);
21002100
releaseSingletonInstance(deletedFiber.stateNode);
21012101

21022102
hostParent = prevHostParent;
@@ -2624,10 +2624,12 @@ function commitMutationEffectsOnFiber(
26242624
const previousWork = finishedWork.alternate;
26252625
if (previousWork === null) {
26262626
const singleton = finishedWork.stateNode;
2627-
clearContainer(singleton);
2627+
const props = finishedWork.memoizedProps;
2628+
// This was a new mount, we need to clear and set initial properties
2629+
clearSingleton(singleton);
26282630
acquireSingletonInstance(
26292631
finishedWork.type,
2630-
finishedWork.memoizedProps,
2632+
props,
26312633
singleton,
26322634
finishedWork,
26332635
);

packages/react-reconciler/src/ReactFiberCommitWork.old.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ import {
150150
detachDeletedInstance,
151151
acquireResource,
152152
releaseResource,
153+
clearSingleton,
153154
acquireSingletonInstance,
154155
releaseSingletonInstance,
155156
} from './ReactFiberHostConfig';
@@ -2096,7 +2097,6 @@ function commitDeletionEffectsOnFiber(
20962097
// a different fiber. To increase our chances of avoiding this, specifically
20972098
// if you keyed a HostSingleton so there will be a delete followed by a Placement
20982099
// we treat detach eagerly here
2099-
detachDeletedInstance(deletedFiber.stateNode);
21002100
releaseSingletonInstance(deletedFiber.stateNode);
21012101

21022102
hostParent = prevHostParent;
@@ -2624,10 +2624,12 @@ function commitMutationEffectsOnFiber(
26242624
const previousWork = finishedWork.alternate;
26252625
if (previousWork === null) {
26262626
const singleton = finishedWork.stateNode;
2627-
clearContainer(singleton);
2627+
const props = finishedWork.memoizedProps;
2628+
// This was a new mount, we need to clear and set initial properties
2629+
clearSingleton(singleton);
26282630
acquireSingletonInstance(
26292631
finishedWork.type,
2630-
finishedWork.memoizedProps,
2632+
props,
26312633
singleton,
26322634
finishedWork,
26332635
);

packages/react-reconciler/src/ReactFiberCompleteWork.new.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,9 +1022,9 @@ function completeWork(
10221022
const currentHostContext = getHostContext();
10231023
const wasHydrated = popHydrationState(workInProgress);
10241024
if (wasHydrated) {
1025-
// This method returns an updateQueue. we intentionally ignore the queue
1026-
// because HostSingletons mount with child fibers even if they have a single
1027-
// Text only child and we do not update attributes on hydrated nodes
1025+
// We ignore the boolean indicating there is an updateQueue because
1026+
// it is used only to set text children and HostSingletons do not
1027+
// use them.
10281028
prepareToHydrateHostInstance(workInProgress, currentHostContext);
10291029
} else {
10301030
workInProgress.stateNode = resolveSingletonInstance(

packages/react-reconciler/src/ReactFiberCompleteWork.old.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,9 +1022,9 @@ function completeWork(
10221022
const currentHostContext = getHostContext();
10231023
const wasHydrated = popHydrationState(workInProgress);
10241024
if (wasHydrated) {
1025-
// This method returns an updateQueue. we intentionally ignore the queue
1026-
// because HostSingletons mount with child fibers even if they have a single
1027-
// Text only child and we do not update attributes on hydrated nodes
1025+
// We ignore the boolean indicating there is an updateQueue because
1026+
// it is used only to set text children and HostSingletons do not
1027+
// use them.
10281028
prepareToHydrateHostInstance(workInProgress, currentHostContext);
10291029
} else {
10301030
workInProgress.stateNode = resolveSingletonInstance(

0 commit comments

Comments
 (0)