Skip to content

Commit ea2af87

Browse files
author
Brian Vaughn
authored
Root API should clear non-empty roots before mounting (facebook#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.
1 parent d2ef120 commit ea2af87

File tree

13 files changed

+110
-7
lines changed

13 files changed

+110
-7
lines changed

packages/react-art/src/ReactARTHostConfig.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,10 @@ export function unhideTextInstance(textInstance, text): void {
427427
// Noop
428428
}
429429

430+
export function clearContainer(container) {
431+
// TODO Implement this
432+
}
433+
430434
export function DEPRECATED_mountResponderInstance(
431435
responder: ReactEventResponder<any, any>,
432436
responderInstance: ReactEventResponderInstance<any, any>,

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

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,28 @@ describe('ReactDOMRoot', () => {
113113
expect(() => Scheduler.unstable_flushAll()).toErrorDev('Extra attributes');
114114
});
115115

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 () => {
117138
container.innerHTML = '<div>a</div><div>b</div>';
118139
const root = ReactDOM.createRoot(container);
119140
root.render(
@@ -123,15 +144,15 @@ describe('ReactDOMRoot', () => {
123144
</div>,
124145
);
125146
Scheduler.unstable_flushAll();
126-
expect(container.textContent).toEqual('abcd');
147+
expect(container.textContent).toEqual('cd');
127148
root.render(
128149
<div>
129150
<span>d</span>
130151
<span>c</span>
131152
</div>,
132153
);
133154
Scheduler.unstable_flushAll();
134-
expect(container.textContent).toEqual('abdc');
155+
expect(container.textContent).toEqual('dc');
135156
});
136157

137158
it('throws a good message on invalid containers', () => {
@@ -220,7 +241,14 @@ describe('ReactDOMRoot', () => {
220241
let unmounted = false;
221242
expect(() => {
222243
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+
);
224252
expect(unmounted).toBe(false);
225253
Scheduler.unstable_flushAll();
226254
expect(container.textContent).toEqual('Hi');

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,17 @@ export function unhideTextInstance(
634634
textInstance.nodeValue = text;
635635
}
636636

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+
637648
// -------------------
638649
// Hydration
639650
// -------------------

packages/react-native-renderer/src/ReactNativeHostConfig.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,11 @@ export function unhideInstance(instance: Instance, props: Props): void {
482482
);
483483
}
484484

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+
485490
export function unhideTextInstance(
486491
textInstance: TextInstance,
487492
text: string,

packages/react-noop-renderer/src/createReactNoop.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
155155
insertInContainerOrInstanceBefore(parentInstance, child, beforeChild);
156156
}
157157

158+
function clearContainer(container: Container): void {
159+
container.children.splice(0);
160+
}
161+
158162
function removeChildFromContainerOrInstance(
159163
parentInstance: Container | Instance,
160164
child: Instance | TextInstance,
@@ -502,6 +506,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
502506
insertInContainerBefore,
503507
removeChild,
504508
removeChildFromContainer,
509+
clearContainer,
505510

506511
hideInstance(instance: Instance): void {
507512
instance.hidden = true;
@@ -531,6 +536,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
531536
supportsPersistence: true,
532537

533538
cloneInstance,
539+
clearContainer,
534540

535541
createContainerChildSet(
536542
container: Container,

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ import {
113113
commitHydratedContainer,
114114
commitHydratedSuspenseInstance,
115115
beforeRemoveInstance,
116+
clearContainer,
116117
} from './ReactFiberHostConfig';
117118
import {
118119
captureCommitPhaseError,
@@ -295,7 +296,15 @@ function commitBeforeMutationLifeCycles(
295296
}
296297
return;
297298
}
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+
}
299308
case HostComponent:
300309
case HostText:
301310
case HostPortal:

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ import {
111111
commitHydratedContainer,
112112
commitHydratedSuspenseInstance,
113113
beforeRemoveInstance,
114+
clearContainer,
114115
} from './ReactFiberHostConfig';
115116
import {
116117
captureCommitPhaseError,
@@ -293,7 +294,15 @@ function commitBeforeMutationLifeCycles(
293294
}
294295
return;
295296
}
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+
}
297306
case HostComponent:
298307
case HostText:
299308
case HostPortal:

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,13 @@ import {
5858
OffscreenComponent,
5959
} from './ReactWorkTags';
6060
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';
6268
import invariant from 'shared/invariant';
6369

6470
import {
@@ -675,6 +681,14 @@ function completeWork(
675681
// If we hydrated, then we'll need to schedule an update for
676682
// the commit side-effects on the root.
677683
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;
678692
}
679693
}
680694
updateHostContainer(workInProgress);

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import {
6161
NoEffect,
6262
DidCapture,
6363
Deletion,
64+
Snapshot,
6465
} from './ReactSideEffectTags';
6566
import invariant from 'shared/invariant';
6667

@@ -678,6 +679,14 @@ function completeWork(
678679
// If we hydrated, then we'll need to schedule an update for
679680
// the commit side-effects on the root.
680681
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;
681690
}
682691
}
683692
updateHostContainer(workInProgress);

packages/react-reconciler/src/ReactFiberHostConfigWithNoMutation.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,4 @@ export const hideInstance = shim;
3737
export const hideTextInstance = shim;
3838
export const unhideInstance = shim;
3939
export const unhideTextInstance = shim;
40+
export const clearContainer = shim;

packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ describe('ReactFiberHostContext', () => {
5353
appendChildToContainer: function() {
5454
return null;
5555
},
56+
clearContainer: function() {},
5657
supportsMutation: true,
5758
});
5859

@@ -107,6 +108,7 @@ describe('ReactFiberHostContext', () => {
107108
appendChildToContainer: function() {
108109
return null;
109110
},
111+
clearContainer: function() {},
110112
supportsMutation: true,
111113
});
112114

packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ export const updateFundamentalComponent =
107107
$$$hostConfig.updateFundamentalComponent;
108108
export const unmountFundamentalComponent =
109109
$$$hostConfig.unmountFundamentalComponent;
110+
export const clearContainer = $$$hostConfig.clearContainer;
110111

111112
// -------------------
112113
// Persistence

packages/react-test-renderer/src/ReactTestHostConfig.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,10 @@ export function removeChild(
126126
parentInstance.children.splice(index, 1);
127127
}
128128

129+
export function clearContainer(container: Container): void {
130+
container.children.splice(0);
131+
}
132+
129133
export function getRootHostContext(
130134
rootContainerInstance: Container,
131135
): HostContext {

0 commit comments

Comments
 (0)