Root API should clear non-empty roots before mounting (#18730)
* Root API should clear non-empty roots before mounting Legacy render-into-subtree API removes children from a container before rendering into it. The root API did not do this previously, but just left the children around in the document. This commit adds a new FiberRoot flag to clear a container's contents before mounting. This is done during the commit phase, to avoid multiple, observable mutations.
Brian Vaughn committed
Apr 28, 2020 at 13:07 UTC
ea2af878cc3fb139b0e08cf9bc4b2f4178429d69
13 files changed
+110
-7
packages/react-art/src/ReactARTHostConfig.js
+4
@@ -427,6 +427,10 @@ export function unhideTextInstance(textInstance, text): void {
427
// Noop
428
}
429
430
+export function clearContainer(container) {
431
+ // TODO Implement this
432
+}
433
+
434
export function DEPRECATED_mountResponderInstance(
435
responder: ReactEventResponder<any, any>,
436
responderInstance: ReactEventResponderInstance<any, any>,
packages/react-dom/src/__tests__/ReactDOMRoot-test.js
+32
-4
@@ -113,7 +113,28 @@ describe('ReactDOMRoot', () => {
113
expect(() => Scheduler.unstable_flushAll()).toErrorDev('Extra attributes');
114
});
115
116
- it('does not clear existing children', async () => {
116
+ it('clears existing children with legacy API', async () => {
117
+ container.innerHTML = '<div>a</div><div>b</div>';
118
+ ReactDOM.render(
119
+ <div>
120
+ <span>c</span>
121
+ <span>d</span>
122
+ </div>,
123
+ container,
124
+ );
125
+ expect(container.textContent).toEqual('cd');
126
+ ReactDOM.render(
127
+ <div>
128
+ <span>d</span>
129
+ <span>c</span>
130
+ </div>,
131
+ container,
132
+ );
133
+ Scheduler.unstable_flushAll();
134
+ expect(container.textContent).toEqual('dc');
135
+ });
136
+
137
+ it('clears existing children', async () => {
138
container.innerHTML = '<div>a</div><div>b</div>';
139
const root = ReactDOM.createRoot(container);
140
root.render(
@@ -123,7 +144,7 @@ describe('ReactDOMRoot', () => {
144
</div>,
145
);
146
Scheduler.unstable_flushAll();
126
- expect(container.textContent).toEqual('abcd');
147
+ expect(container.textContent).toEqual('cd');
148
root.render(
149
<div>
150
<span>d</span>
@@ -131,7 +152,7 @@ describe('ReactDOMRoot', () => {
152
</div>,
153
);
154
Scheduler.unstable_flushAll();
134
- expect(container.textContent).toEqual('abdc');
155
+ expect(container.textContent).toEqual('dc');
156
});
157
158
it('throws a good message on invalid containers', () => {
@@ -220,7 +241,14 @@ describe('ReactDOMRoot', () => {
241
let unmounted = false;
242
expect(() => {
243
unmounted = ReactDOM.unmountComponentAtNode(container);
223
- }).toErrorDev('Did you mean to call root.unmount()?', {withoutStack: true});
244
+ }).toErrorDev(
245
+ [
246
+ 'Did you mean to call root.unmount()?',
247
+ // This is more of a symptom but restructuring the code to avoid it isn't worth it:
248
+ "The node you're attempting to unmount was rendered by React and is not a top-level container.",
249
+ ],
250
+ {withoutStack: true},
251
+ );
252
expect(unmounted).toBe(false);
253
Scheduler.unstable_flushAll();
254
expect(container.textContent).toEqual('Hi');
packages/react-dom/src/client/ReactDOMHostConfig.js
+11
@@ -634,6 +634,17 @@ export function unhideTextInstance(
634
textInstance.nodeValue = text;
635
}
636
637
+export function clearContainer(container: Container): void {
638
+ if (container.nodeType === ELEMENT_NODE) {
639
+ ((container: any): Element).textContent = '';
640
+ } else if (container.nodeType === DOCUMENT_NODE) {
641
+ const body = ((container: any): Document).body;
642
+ if (body != null) {
643
+ body.textContent = '';
644
+ }
645
+ }
646
+}
647
+
648
// -------------------
649
// Hydration
650
// -------------------
packages/react-native-renderer/src/ReactNativeHostConfig.js
+5
@@ -482,6 +482,11 @@ export function unhideInstance(instance: Instance, props: Props): void {
482
);
483
}
484
485
+export function clearContainer(container: Container): void {
486
+ // TODO Implement this for React Native
487
+ // UIManager does not expose a "remove all" type method.
488
+}
489
+
490
export function unhideTextInstance(
491
textInstance: TextInstance,
492
text: string,
packages/react-noop-renderer/src/createReactNoop.js
+6
@@ -155,6 +155,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
155
insertInContainerOrInstanceBefore(parentInstance, child, beforeChild);
156
}
157
158
+ function clearContainer(container: Container): void {
159
+ container.children.splice(0);
160
+ }
161
+
162
function removeChildFromContainerOrInstance(
163
parentInstance: Container | Instance,
164
child: Instance | TextInstance,
@@ -502,6 +506,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
506
insertInContainerBefore,
507
removeChild,
508
removeChildFromContainer,
509
+ clearContainer,
510
511
hideInstance(instance: Instance): void {
512
instance.hidden = true;
@@ -531,6 +536,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
536
supportsPersistence: true,
537
538
cloneInstance,
539
+ clearContainer,
540
541
createContainerChildSet(
542
container: Container,
packages/react-reconciler/src/ReactFiberCommitWork.new.js
+10
-1
@@ -113,6 +113,7 @@ import {
113
commitHydratedContainer,
114
commitHydratedSuspenseInstance,
115
beforeRemoveInstance,
116
+ clearContainer,
117
} from './ReactFiberHostConfig';
118
import {
119
captureCommitPhaseError,
@@ -295,7 +296,15 @@ function commitBeforeMutationLifeCycles(
296
}
297
return;
298
}
298
- case HostRoot:
299
+ case HostRoot: {
300
+ if (supportsMutation) {
301
+ if (finishedWork.effectTag & Snapshot) {
302
+ const root = finishedWork.stateNode;
303
+ clearContainer(root.containerInfo);
304
+ }
305
+ }
306
+ return;
307
+ }
308
case HostComponent:
309
case HostText:
310
case HostPortal:
packages/react-reconciler/src/ReactFiberCommitWork.old.js
+10
-1
@@ -111,6 +111,7 @@ import {
111
commitHydratedContainer,
112
commitHydratedSuspenseInstance,
113
beforeRemoveInstance,
114
+ clearContainer,
115
} from './ReactFiberHostConfig';
116
import {
117
captureCommitPhaseError,
@@ -293,7 +294,15 @@ function commitBeforeMutationLifeCycles(
294
}
295
return;
296
}
296
- case HostRoot:
297
+ case HostRoot: {
298
+ if (supportsMutation) {
299
+ if (finishedWork.effectTag & Snapshot) {
300
+ const root = finishedWork.stateNode;
301
+ clearContainer(root.containerInfo);
302
+ }
303
+ }
304
+ return;
305
+ }
306
case HostComponent:
307
case HostText:
308
case HostPortal:
packages/react-reconciler/src/ReactFiberCompleteWork.new.js
+15
-1
@@ -58,7 +58,13 @@ import {
58
OffscreenComponent,
59
} from './ReactWorkTags';
60
import {NoMode, BlockingMode} from './ReactTypeOfMode';
61
-import {Ref, Update, NoEffect, DidCapture} from './ReactSideEffectTags';
61
+import {
62
+ Ref,
63
+ Update,
64
+ NoEffect,
65
+ DidCapture,
66
+ Snapshot,
67
+} from './ReactSideEffectTags';
68
import invariant from 'shared/invariant';
69
70
import {
@@ -675,6 +681,14 @@ function completeWork(
681
// If we hydrated, then we'll need to schedule an update for
682
// the commit side-effects on the root.
683
markUpdate(workInProgress);
684
+ } else if (!fiberRoot.hydrate) {
685
+ // Schedule an effect to clear this container at the start of the next commit.
686
+ // This handles the case of React rendering into a container with previous children.
687
+ // It's also safe to do for updates too, because current.child would only be null
688
+ // if the previous render was null (so the the container would already be empty).
689
+ //
690
+ // The additional root.hydrate check is required for hydration in legacy mode with no fallback.
691
+ workInProgress.effectTag |= Snapshot;
692
}
693
}
694
updateHostContainer(workInProgress);
packages/react-reconciler/src/ReactFiberCompleteWork.old.js
+9
@@ -61,6 +61,7 @@ import {
61
NoEffect,
62
DidCapture,
63
Deletion,
64
+ Snapshot,
65
} from './ReactSideEffectTags';
66
import invariant from 'shared/invariant';
67
@@ -678,6 +679,14 @@ function completeWork(
679
// If we hydrated, then we'll need to schedule an update for
680
// the commit side-effects on the root.
681
markUpdate(workInProgress);
682
+ } else if (!fiberRoot.hydrate) {
683
+ // Schedule an effect to clear this container at the start of the next commit.
684
+ // This handles the case of React rendering into a container with previous children.
685
+ // It's also safe to do for updates too, because current.child would only be null
686
+ // if the previous render was null (so the the container would already be empty).
687
+ //
688
+ // The additional root.hydrate check is required for hydration in legacy mode with no fallback.
689
+ workInProgress.effectTag |= Snapshot;
690
}
691
}
692
updateHostContainer(workInProgress);
packages/react-reconciler/src/ReactFiberHostConfigWithNoMutation.js
+1
@@ -37,3 +37,4 @@ export const hideInstance = shim;
37
export const hideTextInstance = shim;
38
export const unhideInstance = shim;
39
export const unhideTextInstance = shim;
40
+export const clearContainer = shim;
packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js
+2
@@ -53,6 +53,7 @@ describe('ReactFiberHostContext', () => {
53
appendChildToContainer: function() {
54
return null;
55
},
56
+ clearContainer: function() {},
57
supportsMutation: true,
58
});
59
@@ -107,6 +108,7 @@ describe('ReactFiberHostContext', () => {
108
appendChildToContainer: function() {
109
return null;
110
},
111
+ clearContainer: function() {},
112
supportsMutation: true,
113
});
114
packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
+1
@@ -107,6 +107,7 @@ export const updateFundamentalComponent =
107
$$$hostConfig.updateFundamentalComponent;
108
export const unmountFundamentalComponent =
109
$$$hostConfig.unmountFundamentalComponent;
110
+export const clearContainer = $$$hostConfig.clearContainer;
111
112
// -------------------
113
// Persistence
packages/react-test-renderer/src/ReactTestHostConfig.js
+4
@@ -126,6 +126,10 @@ export function removeChild(
126
parentInstance.children.splice(index, 1);
127
}
128
129
+export function clearContainer(container: Container): void {
130
+ container.children.splice(0);
131
+}
132
+
133
export function getRootHostContext(
134
rootContainerInstance: Container,
135
): HostContext {