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.
This commit is contained in:
Brian Vaughn
2020-04-28 13:07:42 -07:00
committed by GitHub
parent d2ef120089
commit ea2af878cc
13 changed files with 110 additions and 7 deletions
+4
View File
@@ -427,6 +427,10 @@ export function unhideTextInstance(textInstance, text): void {
// Noop
}
export function clearContainer(container) {
// TODO Implement this
}
export function DEPRECATED_mountResponderInstance(
responder: ReactEventResponder<any, any>,
responderInstance: ReactEventResponderInstance<any, any>,
+32 -4
View File
@@ -113,7 +113,28 @@ describe('ReactDOMRoot', () => {
expect(() => Scheduler.unstable_flushAll()).toErrorDev('Extra attributes');
});
it('does not clear existing children', async () => {
it('clears existing children with legacy API', async () => {
container.innerHTML = '<div>a</div><div>b</div>';
ReactDOM.render(
<div>
<span>c</span>
<span>d</span>
</div>,
container,
);
expect(container.textContent).toEqual('cd');
ReactDOM.render(
<div>
<span>d</span>
<span>c</span>
</div>,
container,
);
Scheduler.unstable_flushAll();
expect(container.textContent).toEqual('dc');
});
it('clears existing children', async () => {
container.innerHTML = '<div>a</div><div>b</div>';
const root = ReactDOM.createRoot(container);
root.render(
@@ -123,7 +144,7 @@ describe('ReactDOMRoot', () => {
</div>,
);
Scheduler.unstable_flushAll();
expect(container.textContent).toEqual('abcd');
expect(container.textContent).toEqual('cd');
root.render(
<div>
<span>d</span>
@@ -131,7 +152,7 @@ describe('ReactDOMRoot', () => {
</div>,
);
Scheduler.unstable_flushAll();
expect(container.textContent).toEqual('abdc');
expect(container.textContent).toEqual('dc');
});
it('throws a good message on invalid containers', () => {
@@ -220,7 +241,14 @@ describe('ReactDOMRoot', () => {
let unmounted = false;
expect(() => {
unmounted = ReactDOM.unmountComponentAtNode(container);
}).toErrorDev('Did you mean to call root.unmount()?', {withoutStack: true});
}).toErrorDev(
[
'Did you mean to call root.unmount()?',
// This is more of a symptom but restructuring the code to avoid it isn't worth it:
"The node you're attempting to unmount was rendered by React and is not a top-level container.",
],
{withoutStack: true},
);
expect(unmounted).toBe(false);
Scheduler.unstable_flushAll();
expect(container.textContent).toEqual('Hi');
+11
View File
@@ -634,6 +634,17 @@ export function unhideTextInstance(
textInstance.nodeValue = text;
}
export function clearContainer(container: Container): void {
if (container.nodeType === ELEMENT_NODE) {
((container: any): Element).textContent = '';
} else if (container.nodeType === DOCUMENT_NODE) {
const body = ((container: any): Document).body;
if (body != null) {
body.textContent = '';
}
}
}
// -------------------
// Hydration
// -------------------
@@ -482,6 +482,11 @@ export function unhideInstance(instance: Instance, props: Props): void {
);
}
export function clearContainer(container: Container): void {
// TODO Implement this for React Native
// UIManager does not expose a "remove all" type method.
}
export function unhideTextInstance(
textInstance: TextInstance,
text: string,
+6
View File
@@ -155,6 +155,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
insertInContainerOrInstanceBefore(parentInstance, child, beforeChild);
}
function clearContainer(container: Container): void {
container.children.splice(0);
}
function removeChildFromContainerOrInstance(
parentInstance: Container | Instance,
child: Instance | TextInstance,
@@ -502,6 +506,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
insertInContainerBefore,
removeChild,
removeChildFromContainer,
clearContainer,
hideInstance(instance: Instance): void {
instance.hidden = true;
@@ -531,6 +536,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
supportsPersistence: true,
cloneInstance,
clearContainer,
createContainerChildSet(
container: Container,
@@ -113,6 +113,7 @@ import {
commitHydratedContainer,
commitHydratedSuspenseInstance,
beforeRemoveInstance,
clearContainer,
} from './ReactFiberHostConfig';
import {
captureCommitPhaseError,
@@ -295,7 +296,15 @@ function commitBeforeMutationLifeCycles(
}
return;
}
case HostRoot:
case HostRoot: {
if (supportsMutation) {
if (finishedWork.effectTag & Snapshot) {
const root = finishedWork.stateNode;
clearContainer(root.containerInfo);
}
}
return;
}
case HostComponent:
case HostText:
case HostPortal:
@@ -111,6 +111,7 @@ import {
commitHydratedContainer,
commitHydratedSuspenseInstance,
beforeRemoveInstance,
clearContainer,
} from './ReactFiberHostConfig';
import {
captureCommitPhaseError,
@@ -293,7 +294,15 @@ function commitBeforeMutationLifeCycles(
}
return;
}
case HostRoot:
case HostRoot: {
if (supportsMutation) {
if (finishedWork.effectTag & Snapshot) {
const root = finishedWork.stateNode;
clearContainer(root.containerInfo);
}
}
return;
}
case HostComponent:
case HostText:
case HostPortal:
@@ -58,7 +58,13 @@ import {
OffscreenComponent,
} from './ReactWorkTags';
import {NoMode, BlockingMode} from './ReactTypeOfMode';
import {Ref, Update, NoEffect, DidCapture} from './ReactSideEffectTags';
import {
Ref,
Update,
NoEffect,
DidCapture,
Snapshot,
} from './ReactSideEffectTags';
import invariant from 'shared/invariant';
import {
@@ -675,6 +681,14 @@ function completeWork(
// If we hydrated, then we'll need to schedule an update for
// the commit side-effects on the root.
markUpdate(workInProgress);
} else if (!fiberRoot.hydrate) {
// Schedule an effect to clear this container at the start of the next commit.
// This handles the case of React rendering into a container with previous children.
// It's also safe to do for updates too, because current.child would only be null
// if the previous render was null (so the the container would already be empty).
//
// The additional root.hydrate check is required for hydration in legacy mode with no fallback.
workInProgress.effectTag |= Snapshot;
}
}
updateHostContainer(workInProgress);
@@ -61,6 +61,7 @@ import {
NoEffect,
DidCapture,
Deletion,
Snapshot,
} from './ReactSideEffectTags';
import invariant from 'shared/invariant';
@@ -678,6 +679,14 @@ function completeWork(
// If we hydrated, then we'll need to schedule an update for
// the commit side-effects on the root.
markUpdate(workInProgress);
} else if (!fiberRoot.hydrate) {
// Schedule an effect to clear this container at the start of the next commit.
// This handles the case of React rendering into a container with previous children.
// It's also safe to do for updates too, because current.child would only be null
// if the previous render was null (so the the container would already be empty).
//
// The additional root.hydrate check is required for hydration in legacy mode with no fallback.
workInProgress.effectTag |= Snapshot;
}
}
updateHostContainer(workInProgress);
@@ -37,3 +37,4 @@ export const hideInstance = shim;
export const hideTextInstance = shim;
export const unhideInstance = shim;
export const unhideTextInstance = shim;
export const clearContainer = shim;
@@ -53,6 +53,7 @@ describe('ReactFiberHostContext', () => {
appendChildToContainer: function() {
return null;
},
clearContainer: function() {},
supportsMutation: true,
});
@@ -107,6 +108,7 @@ describe('ReactFiberHostContext', () => {
appendChildToContainer: function() {
return null;
},
clearContainer: function() {},
supportsMutation: true,
});
@@ -107,6 +107,7 @@ export const updateFundamentalComponent =
$$$hostConfig.updateFundamentalComponent;
export const unmountFundamentalComponent =
$$$hostConfig.unmountFundamentalComponent;
export const clearContainer = $$$hostConfig.clearContainer;
// -------------------
// Persistence
@@ -126,6 +126,10 @@ export function removeChild(
parentInstance.children.splice(index, 1);
}
export function clearContainer(container: Container): void {
container.children.splice(0);
}
export function getRootHostContext(
rootContainerInstance: Container,
): HostContext {