diff --git a/packages/react-art/src/ReactARTHostConfig.js b/packages/react-art/src/ReactARTHostConfig.js index 7086dcccb5f0c..18395feffcd99 100644 --- a/packages/react-art/src/ReactARTHostConfig.js +++ b/packages/react-art/src/ReactARTHostConfig.js @@ -427,6 +427,10 @@ export function unhideTextInstance(textInstance, text): void { // Noop } +export function clearContainer(container) { + // TODO Implement this +} + export function DEPRECATED_mountResponderInstance( responder: ReactEventResponder, responderInstance: ReactEventResponderInstance, diff --git a/packages/react-dom/src/__tests__/ReactDOMRoot-test.js b/packages/react-dom/src/__tests__/ReactDOMRoot-test.js index 0d842481b0180..29acb9ba15820 100644 --- a/packages/react-dom/src/__tests__/ReactDOMRoot-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMRoot-test.js @@ -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 = '
a
b
'; + ReactDOM.render( +
+ c + d +
, + container, + ); + expect(container.textContent).toEqual('cd'); + ReactDOM.render( +
+ d + c +
, + container, + ); + Scheduler.unstable_flushAll(); + expect(container.textContent).toEqual('dc'); + }); + + it('clears existing children', async () => { container.innerHTML = '
a
b
'; const root = ReactDOM.createRoot(container); root.render( @@ -123,7 +144,7 @@ describe('ReactDOMRoot', () => { , ); Scheduler.unstable_flushAll(); - expect(container.textContent).toEqual('abcd'); + expect(container.textContent).toEqual('cd'); root.render(
d @@ -131,7 +152,7 @@ describe('ReactDOMRoot', () => {
, ); 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'); diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js index dd7ac44686a34..0fbeaed352447 100644 --- a/packages/react-dom/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -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 // ------------------- diff --git a/packages/react-native-renderer/src/ReactNativeHostConfig.js b/packages/react-native-renderer/src/ReactNativeHostConfig.js index fd9b08cf4b0a9..5fe7ffd1bf915 100644 --- a/packages/react-native-renderer/src/ReactNativeHostConfig.js +++ b/packages/react-native-renderer/src/ReactNativeHostConfig.js @@ -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, diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index d8b5b72806bc3..227b24d3b9136 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -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, diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js index bb8799bab3fd3..2e430f94b9a1c 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js @@ -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: diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.old.js b/packages/react-reconciler/src/ReactFiberCommitWork.old.js index 87b3847d0d5ec..5b65fdded432a 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.old.js @@ -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: diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js index 12e2052f34c37..3caaba82979be 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js @@ -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); diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js index ee12b05bfd193..fe032eba86817 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js @@ -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); diff --git a/packages/react-reconciler/src/ReactFiberHostConfigWithNoMutation.js b/packages/react-reconciler/src/ReactFiberHostConfigWithNoMutation.js index badb80920b162..50e6758a8d3e1 100644 --- a/packages/react-reconciler/src/ReactFiberHostConfigWithNoMutation.js +++ b/packages/react-reconciler/src/ReactFiberHostConfigWithNoMutation.js @@ -37,3 +37,4 @@ export const hideInstance = shim; export const hideTextInstance = shim; export const unhideInstance = shim; export const unhideTextInstance = shim; +export const clearContainer = shim; diff --git a/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js b/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js index 1237a89c74532..5eda4dc7c7cd0 100644 --- a/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js @@ -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, }); diff --git a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js index 6bc1c38dafce9..f88e48e6dfa21 100644 --- a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js +++ b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js @@ -107,6 +107,7 @@ export const updateFundamentalComponent = $$$hostConfig.updateFundamentalComponent; export const unmountFundamentalComponent = $$$hostConfig.unmountFundamentalComponent; +export const clearContainer = $$$hostConfig.clearContainer; // ------------------- // Persistence diff --git a/packages/react-test-renderer/src/ReactTestHostConfig.js b/packages/react-test-renderer/src/ReactTestHostConfig.js index 296bdcf56db08..23454de62e842 100644 --- a/packages/react-test-renderer/src/ReactTestHostConfig.js +++ b/packages/react-test-renderer/src/ReactTestHostConfig.js @@ -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 {