diff --git a/packages/react-dom/src/__tests__/ReactDOMRoot-test.js b/packages/react-dom/src/__tests__/ReactDOMRoot-test.js index 4157e64b46..fd6292385b 100644 --- a/packages/react-dom/src/__tests__/ReactDOMRoot-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMRoot-test.js @@ -374,4 +374,104 @@ describe('ReactDOMRoot', () => { 'unstable_createRoot(...): Target container is not a DOM element.', ); }); + + it('warns when rendering with legacy API into createRoot() container', () => { + const root = ReactDOM.unstable_createRoot(container); + root.render(
Hi
); + jest.runAllTimers(); + expect(container.textContent).toEqual('Hi'); + expect(() => { + ReactDOM.render(
Bye
, container); + }).toWarnDev( + [ + // We care about this warning: + 'You are calling ReactDOM.render() on a container that was previously ' + + 'passed to ReactDOM.unstable_createRoot(). This is not supported. ' + + 'Did you mean to call root.render(element)?', + // This is more of a symptom but restructuring the code to avoid it isn't worth it: + 'Replacing React-rendered children with a new root component.', + ], + {withoutStack: true}, + ); + jest.runAllTimers(); + // This works now but we could disallow it: + expect(container.textContent).toEqual('Bye'); + }); + + it('warns when hydrating with legacy API into createRoot() container', () => { + const root = ReactDOM.unstable_createRoot(container); + root.render(
Hi
); + jest.runAllTimers(); + expect(container.textContent).toEqual('Hi'); + expect(() => { + ReactDOM.hydrate(
Hi
, container); + }).toWarnDev( + [ + // We care about this warning: + 'You are calling ReactDOM.hydrate() on a container that was previously ' + + 'passed to ReactDOM.unstable_createRoot(). This is not supported. ' + + 'Did you mean to call root.render(element, {hydrate: true})?', + // This is more of a symptom but restructuring the code to avoid it isn't worth it: + 'Replacing React-rendered children with a new root component.', + ], + {withoutStack: true}, + ); + }); + + it('warns when unmounting with legacy API (no previous content)', () => { + const root = ReactDOM.unstable_createRoot(container); + root.render(
Hi
); + jest.runAllTimers(); + expect(container.textContent).toEqual('Hi'); + let unmounted = false; + expect(() => { + unmounted = ReactDOM.unmountComponentAtNode(container); + }).toWarnDev( + [ + // We care about this warning: + 'You are calling ReactDOM.unmountComponentAtNode() on a container that was previously ' + + 'passed to ReactDOM.unstable_createRoot(). This is not supported. 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); + jest.runAllTimers(); + expect(container.textContent).toEqual('Hi'); + root.unmount(); + jest.runAllTimers(); + expect(container.textContent).toEqual(''); + }); + + it('warns when unmounting with legacy API (has previous content)', () => { + // Currently createRoot().render() doesn't clear this. + container.appendChild(document.createElement('div')); + // The rest is the same as test above. + const root = ReactDOM.unstable_createRoot(container); + root.render(
Hi
); + jest.runAllTimers(); + expect(container.textContent).toEqual('Hi'); + let unmounted = false; + expect(() => { + unmounted = ReactDOM.unmountComponentAtNode(container); + }).toWarnDev('Did you mean to call root.unmount()?', {withoutStack: true}); + expect(unmounted).toBe(false); + jest.runAllTimers(); + expect(container.textContent).toEqual('Hi'); + root.unmount(); + jest.runAllTimers(); + expect(container.textContent).toEqual(''); + }); + + it('warns when passing legacy container to createRoot()', () => { + ReactDOM.render(
Hi
, container); + expect(() => { + ReactDOM.unstable_createRoot(container); + }).toWarnDev( + 'You are calling ReactDOM.unstable_createRoot() on a container that was previously ' + + 'passed to ReactDOM.render(). This is not supported.', + {withoutStack: true}, + ); + }); }); diff --git a/packages/react-dom/src/client/ReactDOM.js b/packages/react-dom/src/client/ReactDOM.js index ded53f4e1c..80251c2ac2 100644 --- a/packages/react-dom/src/client/ReactDOM.js +++ b/packages/react-dom/src/client/ReactDOM.js @@ -14,7 +14,6 @@ import type { FiberRoot, Batch as FiberRootBatch, } from 'react-reconciler/src/ReactFiberRoot'; -import type {Container} from './ReactDOMHostConfig'; import '../shared/checkReact'; import './ReactDOMClientInjection'; @@ -160,9 +159,11 @@ setRestoreImplementation(restoreControlledState); export type DOMContainer = | (Element & { _reactRootContainer: ?Root, + _reactHasBeenPassedToCreateRootDEV: ?boolean, }) | (Document & { _reactRootContainer: ?Root, + _reactHasBeenPassedToCreateRootDEV: ?boolean, }); type Batch = FiberRootBatch & { @@ -362,7 +363,7 @@ ReactWork.prototype._onCommit = function(): void { }; function ReactRoot( - container: Container, + container: DOMContainer, isConcurrent: boolean, hydrate: boolean, ) { @@ -543,12 +544,6 @@ function legacyRenderSubtreeIntoContainer( forceHydrate: boolean, callback: ?Function, ) { - // TODO: Ensure all entry points contain this check - invariant( - isValidContainer(container), - 'Target container is not a DOM element.', - ); - if (__DEV__) { topLevelUpdateWarnings(container); } @@ -652,6 +647,19 @@ const ReactDOM: Object = { }, hydrate(element: React$Node, container: DOMContainer, callback: ?Function) { + invariant( + isValidContainer(container), + 'Target container is not a DOM element.', + ); + if (__DEV__) { + warningWithoutStack( + !container._reactHasBeenPassedToCreateRootDEV, + 'You are calling ReactDOM.hydrate() on a container that was previously ' + + 'passed to ReactDOM.%s(). This is not supported. ' + + 'Did you mean to call root.render(element, {hydrate: true})?', + enableStableConcurrentModeAPIs ? 'createRoot' : 'unstable_createRoot', + ); + } // TODO: throw or warn if we couldn't hydrate? return legacyRenderSubtreeIntoContainer( null, @@ -667,6 +675,19 @@ const ReactDOM: Object = { container: DOMContainer, callback: ?Function, ) { + invariant( + isValidContainer(container), + 'Target container is not a DOM element.', + ); + if (__DEV__) { + warningWithoutStack( + !container._reactHasBeenPassedToCreateRootDEV, + 'You are calling ReactDOM.render() on a container that was previously ' + + 'passed to ReactDOM.%s(). This is not supported. ' + + 'Did you mean to call root.render(element)?', + enableStableConcurrentModeAPIs ? 'createRoot' : 'unstable_createRoot', + ); + } return legacyRenderSubtreeIntoContainer( null, element, @@ -682,6 +703,10 @@ const ReactDOM: Object = { containerNode: DOMContainer, callback: ?Function, ) { + invariant( + isValidContainer(containerNode), + 'Target container is not a DOM element.', + ); invariant( parentComponent != null && hasInstance(parentComponent), 'parentComponent must be a valid React Component', @@ -701,6 +726,15 @@ const ReactDOM: Object = { 'unmountComponentAtNode(...): Target container is not a DOM element.', ); + if (__DEV__) { + warningWithoutStack( + !container._reactHasBeenPassedToCreateRootDEV, + 'You are calling ReactDOM.unmountComponentAtNode() on a container that was previously ' + + 'passed to ReactDOM.%s(). This is not supported. Did you mean to call root.unmount()?', + enableStableConcurrentModeAPIs ? 'createRoot' : 'unstable_createRoot', + ); + } + if (container._reactRootContainer) { if (__DEV__) { const rootEl = getReactRootElementInContainer(container); @@ -805,6 +839,15 @@ function createRoot(container: DOMContainer, options?: RootOptions): ReactRoot { '%s(...): Target container is not a DOM element.', functionName, ); + if (__DEV__) { + warningWithoutStack( + !container._reactRootContainer, + 'You are calling ReactDOM.%s() on a container that was previously ' + + 'passed to ReactDOM.render(). This is not supported.', + enableStableConcurrentModeAPIs ? 'createRoot' : 'unstable_createRoot', + ); + container._reactHasBeenPassedToCreateRootDEV = true; + } const hydrate = options != null && options.hydrate === true; return new ReactRoot(container, true, hydrate); } diff --git a/packages/react-dom/src/fire/ReactFire.js b/packages/react-dom/src/fire/ReactFire.js index b1a7855ab2..58a74eba45 100644 --- a/packages/react-dom/src/fire/ReactFire.js +++ b/packages/react-dom/src/fire/ReactFire.js @@ -19,7 +19,6 @@ import type { FiberRoot, Batch as FiberRootBatch, } from 'react-reconciler/src/ReactFiberRoot'; -import type {Container} from '../client/ReactDOMHostConfig'; import '../shared/checkReact'; import '../client/ReactDOMClientInjection'; @@ -165,9 +164,11 @@ setRestoreImplementation(restoreControlledState); export type DOMContainer = | (Element & { _reactRootContainer: ?Root, + _reactHasBeenPassedToCreateRootDEV: ?boolean, }) | (Document & { _reactRootContainer: ?Root, + _reactHasBeenPassedToCreateRootDEV: ?boolean, }); type Batch = FiberRootBatch & { @@ -367,7 +368,7 @@ ReactWork.prototype._onCommit = function(): void { }; function ReactRoot( - container: Container, + container: DOMContainer, isConcurrent: boolean, hydrate: boolean, ) { @@ -548,12 +549,6 @@ function legacyRenderSubtreeIntoContainer( forceHydrate: boolean, callback: ?Function, ) { - // TODO: Ensure all entry points contain this check - invariant( - isValidContainer(container), - 'Target container is not a DOM element.', - ); - if (__DEV__) { topLevelUpdateWarnings(container); } @@ -657,6 +652,19 @@ const ReactDOM: Object = { }, hydrate(element: React$Node, container: DOMContainer, callback: ?Function) { + invariant( + isValidContainer(container), + 'Target container is not a DOM element.', + ); + if (__DEV__) { + warningWithoutStack( + !container._reactHasBeenPassedToCreateRootDEV, + 'You are calling ReactDOM.hydrate() on a container that was previously ' + + 'passed to ReactDOM.%s(). This is not supported. ' + + 'Did you mean to call root.render(element, {hydrate: true})?', + enableStableConcurrentModeAPIs ? 'createRoot' : 'unstable_createRoot', + ); + } // TODO: throw or warn if we couldn't hydrate? return legacyRenderSubtreeIntoContainer( null, @@ -672,6 +680,19 @@ const ReactDOM: Object = { container: DOMContainer, callback: ?Function, ) { + invariant( + isValidContainer(container), + 'Target container is not a DOM element.', + ); + if (__DEV__) { + warningWithoutStack( + !container._reactHasBeenPassedToCreateRootDEV, + 'You are calling ReactDOM.render() on a container that was previously ' + + 'passed to ReactDOM.%s(). This is not supported. ' + + 'Did you mean to call root.render(element)?', + enableStableConcurrentModeAPIs ? 'createRoot' : 'unstable_createRoot', + ); + } return legacyRenderSubtreeIntoContainer( null, element, @@ -687,6 +708,10 @@ const ReactDOM: Object = { containerNode: DOMContainer, callback: ?Function, ) { + invariant( + isValidContainer(containerNode), + 'Target container is not a DOM element.', + ); invariant( parentComponent != null && hasInstance(parentComponent), 'parentComponent must be a valid React Component', @@ -706,6 +731,15 @@ const ReactDOM: Object = { 'unmountComponentAtNode(...): Target container is not a DOM element.', ); + if (__DEV__) { + warningWithoutStack( + !container._reactHasBeenPassedToCreateRootDEV, + 'You are calling ReactDOM.unmountComponentAtNode() on a container that was previously ' + + 'passed to ReactDOM.%s(). This is not supported. Did you mean to call root.unmount()?', + enableStableConcurrentModeAPIs ? 'createRoot' : 'unstable_createRoot', + ); + } + if (container._reactRootContainer) { if (__DEV__) { const rootEl = getReactRootElementInContainer(container); @@ -810,6 +844,15 @@ function createRoot(container: DOMContainer, options?: RootOptions): ReactRoot { '%s(...): Target container is not a DOM element.', functionName, ); + if (__DEV__) { + warningWithoutStack( + !container._reactRootContainer, + 'You are calling ReactDOM.%s() on a container that was previously ' + + 'passed to ReactDOM.render(). This is not supported.', + enableStableConcurrentModeAPIs ? 'createRoot' : 'unstable_createRoot', + ); + container._reactHasBeenPassedToCreateRootDEV = true; + } const hydrate = options != null && options.hydrate === true; return new ReactRoot(container, true, hydrate); }