Skip to content

Commit a7b4d51

Browse files
authored
Warn when doing createRoot twice on the same node (another approach) (#17329)
* Unify fields used for createRoot warning and event system * Warn when doing createRoot twice on the same node * Stricter check for modern roots * Unmark asynchronously * Fix Flow
1 parent be3bfa6 commit a7b4d51

File tree

3 files changed

+90
-31
lines changed

3 files changed

+90
-31
lines changed

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,4 +212,23 @@ describe('ReactDOMRoot', () => {
212212
{withoutStack: true},
213213
);
214214
});
215+
216+
it('warns when creating two roots managing the same container', () => {
217+
ReactDOM.createRoot(container);
218+
expect(() => {
219+
ReactDOM.createRoot(container);
220+
}).toWarnDev(
221+
'You are calling ReactDOM.createRoot() on a container that ' +
222+
'has already been passed to createRoot() before. Instead, call ' +
223+
'root.render() on the existing root instead if you want to update it.',
224+
{withoutStack: true},
225+
);
226+
});
227+
228+
it('does not warn when creating second root after first one is unmounted', () => {
229+
const root = ReactDOM.createRoot(container);
230+
root.unmount();
231+
Scheduler.unstable_flushAll();
232+
ReactDOM.createRoot(container); // No warning
233+
});
215234
});

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

Lines changed: 63 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@ import {
6868
getNodeFromInstance,
6969
getFiberCurrentPropsFromNode,
7070
getClosestInstanceFromNode,
71+
isContainerMarkedAsRoot,
7172
markContainerAsRoot,
73+
unmarkContainerAsRoot,
7274
} from './ReactDOMComponentTree';
7375
import {restoreControlledState} from './ReactDOMComponent';
7476
import {dispatchEvent} from '../events/ReactDOMEventListener';
@@ -174,11 +176,9 @@ setRestoreImplementation(restoreControlledState);
174176
export type DOMContainer =
175177
| (Element & {
176178
_reactRootContainer: ?_ReactRoot,
177-
_reactHasBeenPassedToCreateRootDEV: ?boolean,
178179
})
179180
| (Document & {
180181
_reactRootContainer: ?_ReactRoot,
181-
_reactHasBeenPassedToCreateRootDEV: ?boolean,
182182
});
183183

184184
type _ReactRoot = {
@@ -226,22 +226,28 @@ ReactRoot.prototype.render = ReactBlockingRoot.prototype.render = function(
226226
callback: ?() => mixed,
227227
): void {
228228
const root = this._internalRoot;
229-
callback = callback === undefined ? null : callback;
229+
const cb = callback === undefined ? null : callback;
230230
if (__DEV__) {
231-
warnOnInvalidCallback(callback, 'render');
231+
warnOnInvalidCallback(cb, 'render');
232232
}
233-
updateContainer(children, root, null, callback);
233+
updateContainer(children, root, null, cb);
234234
};
235235

236236
ReactRoot.prototype.unmount = ReactBlockingRoot.prototype.unmount = function(
237237
callback: ?() => mixed,
238238
): void {
239239
const root = this._internalRoot;
240-
callback = callback === undefined ? null : callback;
240+
const cb = callback === undefined ? null : callback;
241241
if (__DEV__) {
242-
warnOnInvalidCallback(callback, 'render');
242+
warnOnInvalidCallback(cb, 'render');
243243
}
244-
updateContainer(null, root, null, callback);
244+
const container = root.containerInfo;
245+
updateContainer(null, root, null, () => {
246+
unmarkContainerAsRoot(container);
247+
if (cb !== null) {
248+
cb();
249+
}
250+
});
245251
};
246252

247253
/**
@@ -448,12 +454,17 @@ const ReactDOM: Object = {
448454
'Target container is not a DOM element.',
449455
);
450456
if (__DEV__) {
451-
warningWithoutStack(
452-
!container._reactHasBeenPassedToCreateRootDEV,
453-
'You are calling ReactDOM.hydrate() on a container that was previously ' +
454-
'passed to ReactDOM.createRoot(). This is not supported. ' +
455-
'Did you mean to call createRoot(container, {hydrate: true}).render(element)?',
456-
);
457+
const isModernRoot =
458+
isContainerMarkedAsRoot(container) &&
459+
container._reactRootContainer === undefined;
460+
if (isModernRoot) {
461+
warningWithoutStack(
462+
false,
463+
'You are calling ReactDOM.hydrate() on a container that was previously ' +
464+
'passed to ReactDOM.createRoot(). This is not supported. ' +
465+
'Did you mean to call createRoot(container, {hydrate: true}).render(element)?',
466+
);
467+
}
457468
}
458469
// TODO: throw or warn if we couldn't hydrate?
459470
return legacyRenderSubtreeIntoContainer(
@@ -475,12 +486,17 @@ const ReactDOM: Object = {
475486
'Target container is not a DOM element.',
476487
);
477488
if (__DEV__) {
478-
warningWithoutStack(
479-
!container._reactHasBeenPassedToCreateRootDEV,
480-
'You are calling ReactDOM.render() on a container that was previously ' +
481-
'passed to ReactDOM.createRoot(). This is not supported. ' +
482-
'Did you mean to call root.render(element)?',
483-
);
489+
const isModernRoot =
490+
isContainerMarkedAsRoot(container) &&
491+
container._reactRootContainer === undefined;
492+
if (isModernRoot) {
493+
warningWithoutStack(
494+
false,
495+
'You are calling ReactDOM.render() on a container that was previously ' +
496+
'passed to ReactDOM.createRoot(). This is not supported. ' +
497+
'Did you mean to call root.render(element)?',
498+
);
499+
}
484500
}
485501
return legacyRenderSubtreeIntoContainer(
486502
null,
@@ -521,11 +537,16 @@ const ReactDOM: Object = {
521537
);
522538

523539
if (__DEV__) {
524-
warningWithoutStack(
525-
!container._reactHasBeenPassedToCreateRootDEV,
526-
'You are calling ReactDOM.unmountComponentAtNode() on a container that was previously ' +
527-
'passed to ReactDOM.createRoot(). This is not supported. Did you mean to call root.unmount()?',
528-
);
540+
const isModernRoot =
541+
isContainerMarkedAsRoot(container) &&
542+
container._reactRootContainer === undefined;
543+
if (isModernRoot) {
544+
warningWithoutStack(
545+
false,
546+
'You are calling ReactDOM.unmountComponentAtNode() on a container that was previously ' +
547+
'passed to ReactDOM.createRoot(). This is not supported. Did you mean to call root.unmount()?',
548+
);
549+
}
529550
}
530551

531552
if (container._reactRootContainer) {
@@ -543,6 +564,7 @@ const ReactDOM: Object = {
543564
unbatchedUpdates(() => {
544565
legacyRenderSubtreeIntoContainer(null, null, container, false, () => {
545566
container._reactRootContainer = null;
567+
unmarkContainerAsRoot(container);
546568
});
547569
});
548570
// If you call unmountComponentAtNode twice in quick succession, you'll
@@ -650,12 +672,22 @@ function createBlockingRoot(
650672

651673
function warnIfReactDOMContainerInDEV(container) {
652674
if (__DEV__) {
653-
warningWithoutStack(
654-
!container._reactRootContainer,
655-
'You are calling ReactDOM.createRoot() on a container that was previously ' +
656-
'passed to ReactDOM.render(). This is not supported.',
657-
);
658-
container._reactHasBeenPassedToCreateRootDEV = true;
675+
if (isContainerMarkedAsRoot(container)) {
676+
if (container._reactRootContainer) {
677+
warningWithoutStack(
678+
false,
679+
'You are calling ReactDOM.createRoot() on a container that was previously ' +
680+
'passed to ReactDOM.render(). This is not supported.',
681+
);
682+
} else {
683+
warningWithoutStack(
684+
false,
685+
'You are calling ReactDOM.createRoot() on a container that ' +
686+
'has already been passed to createRoot() before. Instead, call ' +
687+
'root.render() on the existing root instead if you want to update it.',
688+
);
689+
}
690+
}
659691
}
660692
}
661693

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ export function markContainerAsRoot(hostRoot, node) {
3030
node[internalContainerInstanceKey] = hostRoot;
3131
}
3232

33+
export function unmarkContainerAsRoot(node) {
34+
node[internalContainerInstanceKey] = null;
35+
}
36+
37+
export function isContainerMarkedAsRoot(node) {
38+
return !!node[internalContainerInstanceKey];
39+
}
40+
3341
// Given a DOM node, return the closest HostComponent or HostText fiber ancestor.
3442
// If the target node is part of a hydrated or not yet rendered subtree, then
3543
// this may also return a SuspenseComponent or HostRoot to indicate that.

0 commit comments

Comments
 (0)