diff --git a/packages/react-dom/src/__tests__/ReactDOMRoot-test.js b/packages/react-dom/src/__tests__/ReactDOMRoot-test.js
index 0c264240976ed..c13c2805a5ccd 100644
--- a/packages/react-dom/src/__tests__/ReactDOMRoot-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMRoot-test.js
@@ -14,6 +14,7 @@ let ReactDOM = require('react-dom');
let ReactDOMServer = require('react-dom/server');
let Scheduler = require('scheduler');
let act;
+let useEffect;
describe('ReactDOMRoot', () => {
let container;
@@ -26,6 +27,7 @@ describe('ReactDOMRoot', () => {
ReactDOMServer = require('react-dom/server');
Scheduler = require('scheduler');
act = require('jest-react').act;
+ useEffect = React.useEffect;
});
it('renders children', () => {
@@ -342,4 +344,62 @@ describe('ReactDOMRoot', () => {
});
expect(container.textContent).toEqual('b');
});
+
+ it('unmount is synchronous', async () => {
+ const root = ReactDOM.createRoot(container);
+ await act(async () => {
+ root.render('Hi');
+ });
+ expect(container.textContent).toEqual('Hi');
+
+ await act(async () => {
+ root.unmount();
+ // Should have already unmounted
+ expect(container.textContent).toEqual('');
+ });
+ });
+
+ it('throws if an unmounted root is updated', async () => {
+ const root = ReactDOM.createRoot(container);
+ await act(async () => {
+ root.render('Hi');
+ });
+ expect(container.textContent).toEqual('Hi');
+
+ root.unmount();
+
+ expect(() => root.render("I'm back")).toThrow(
+ 'Cannot update an unmounted root.',
+ );
+ });
+
+ it('warns if root is unmounted inside an effect', async () => {
+ const container1 = document.createElement('div');
+ const root1 = ReactDOM.createRoot(container1);
+ const container2 = document.createElement('div');
+ const root2 = ReactDOM.createRoot(container2);
+
+ function App({step}) {
+ useEffect(() => {
+ if (step === 2) {
+ root2.unmount();
+ }
+ }, [step]);
+ return 'Hi';
+ }
+
+ await act(async () => {
+ root1.render();
+ });
+ expect(container1.textContent).toEqual('Hi');
+
+ expect(() => {
+ ReactDOM.flushSync(() => {
+ root1.render();
+ });
+ }).toErrorDev(
+ 'Attempted to synchronously unmount a root while React was ' +
+ 'already rendering.',
+ );
+ });
});
diff --git a/packages/react-dom/src/client/ReactDOMRoot.js b/packages/react-dom/src/client/ReactDOMRoot.js
index ba062fc3f71e9..1a027d9a1e6a9 100644
--- a/packages/react-dom/src/client/ReactDOMRoot.js
+++ b/packages/react-dom/src/client/ReactDOMRoot.js
@@ -14,7 +14,7 @@ import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
export type RootType = {
render(children: ReactNodeList): void,
unmount(): void,
- _internalRoot: FiberRoot,
+ _internalRoot: FiberRoot | null,
...
};
@@ -62,17 +62,23 @@ import {
updateContainer,
findHostInstanceWithNoPortals,
registerMutableSourceForHydration,
+ flushSync,
+ isAlreadyRendering,
} from 'react-reconciler/src/ReactFiberReconciler';
import invariant from 'shared/invariant';
import {ConcurrentRoot} from 'react-reconciler/src/ReactRootTags';
import {allowConcurrentByDefault} from 'shared/ReactFeatureFlags';
-function ReactDOMRoot(internalRoot) {
+function ReactDOMRoot(internalRoot: FiberRoot) {
this._internalRoot = internalRoot;
}
ReactDOMRoot.prototype.render = function(children: ReactNodeList): void {
const root = this._internalRoot;
+ if (root === null) {
+ invariant(false, 'Cannot update an unmounted root.');
+ }
+
if (__DEV__) {
if (typeof arguments[1] === 'function') {
console.error(
@@ -109,10 +115,23 @@ ReactDOMRoot.prototype.unmount = function(): void {
}
}
const root = this._internalRoot;
- const container = root.containerInfo;
- updateContainer(null, root, null, () => {
+ if (root !== null) {
+ this._internalRoot = null;
+ const container = root.containerInfo;
+ if (__DEV__) {
+ if (isAlreadyRendering()) {
+ console.error(
+ 'Attempted to synchronously unmount a root while React was already ' +
+ 'rendering. React cannot finish unmounting the root until the ' +
+ 'current render has completed, which may lead to a race condition.',
+ );
+ }
+ }
+ flushSync(() => {
+ updateContainer(null, root, null, null);
+ });
unmarkContainerAsRoot(container);
- });
+ }
};
export function createRoot(
diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json
index 1a6afe0233ed1..3206efa41349c 100644
--- a/scripts/error-codes/codes.json
+++ b/scripts/error-codes/codes.json
@@ -396,5 +396,6 @@
"405": "hydrateRoot(...): Target container is not a DOM element.",
"406": "act(...) is not supported in production builds of React.",
"407": "Missing getServerSnapshot, which is required for server-rendered content. Will revert to client rendering.",
- "408": "Missing getServerSnapshot, which is required for server-rendered content."
+ "408": "Missing getServerSnapshot, which is required for server-rendered content.",
+ "409": "Cannot update an unmounted root."
}