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
Dan Abramov committed
Nov 10, 2019 at 00:54 UTC
a7b4d51a20a00f0f60b113c3a02326ccf69aef82
3 files changed
+90
-31
packages/react-dom/src/__tests__/ReactDOMRoot-test.js
+19
@@ -212,4 +212,23 @@ describe('ReactDOMRoot', () => {
212
{withoutStack: true},
213
);
214
});
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
+ });
234
});
packages/react-dom/src/client/ReactDOM.js
+63
-31
@@ -68,7 +68,9 @@ import {
68
getNodeFromInstance,
69
getFiberCurrentPropsFromNode,
70
getClosestInstanceFromNode,
71
+ isContainerMarkedAsRoot,
72
markContainerAsRoot,
73
+ unmarkContainerAsRoot,
74
} from './ReactDOMComponentTree';
75
import {restoreControlledState} from './ReactDOMComponent';
76
import {dispatchEvent} from '../events/ReactDOMEventListener';
@@ -174,11 +176,9 @@ setRestoreImplementation(restoreControlledState);
176
export type DOMContainer =
177
| (Element & {
178
_reactRootContainer: ?_ReactRoot,
177
- _reactHasBeenPassedToCreateRootDEV: ?boolean,
179
})
180
| (Document & {
181
_reactRootContainer: ?_ReactRoot,
181
- _reactHasBeenPassedToCreateRootDEV: ?boolean,
182
});
183
184
type _ReactRoot = {
@@ -226,22 +226,28 @@ ReactRoot.prototype.render = ReactBlockingRoot.prototype.render = function(
226
callback: ?() => mixed,
227
): void {
228
const root = this._internalRoot;
229
- callback = callback === undefined ? null : callback;
229
+ const cb = callback === undefined ? null : callback;
230
if (__DEV__) {
231
- warnOnInvalidCallback(callback, 'render');
231
+ warnOnInvalidCallback(cb, 'render');
232
}
233
- updateContainer(children, root, null, callback);
233
+ updateContainer(children, root, null, cb);
234
};
235
236
ReactRoot.prototype.unmount = ReactBlockingRoot.prototype.unmount = function(
237
callback: ?() => mixed,
238
): void {
239
const root = this._internalRoot;
240
- callback = callback === undefined ? null : callback;
240
+ const cb = callback === undefined ? null : callback;
241
if (__DEV__) {
242
- warnOnInvalidCallback(callback, 'render');
242
+ warnOnInvalidCallback(cb, 'render');
243
}
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
+ });
251
};
252
253
/**
@@ -448,12 +454,17 @@ const ReactDOM: Object = {
454
'Target container is not a DOM element.',
455
);
456
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
+ }
468
}
469
// TODO: throw or warn if we couldn't hydrate?
470
return legacyRenderSubtreeIntoContainer(
@@ -475,12 +486,17 @@ const ReactDOM: Object = {
486
'Target container is not a DOM element.',
487
);
488
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
+ }
500
}
501
return legacyRenderSubtreeIntoContainer(
502
null,
@@ -521,11 +537,16 @@ const ReactDOM: Object = {
537
);
538
539
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
+ }
550
}
551
552
if (container._reactRootContainer) {
@@ -543,6 +564,7 @@ const ReactDOM: Object = {
564
unbatchedUpdates(() => {
565
legacyRenderSubtreeIntoContainer(null, null, container, false, () => {
566
container._reactRootContainer = null;
567
+ unmarkContainerAsRoot(container);
568
});
569
});
570
// If you call unmountComponentAtNode twice in quick succession, you'll
@@ -650,12 +672,22 @@ function createBlockingRoot(
672
673
function warnIfReactDOMContainerInDEV(container) {
674
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
+ }
691
}
692
}
693
packages/react-dom/src/client/ReactDOMComponentTree.js
+8
@@ -30,6 +30,14 @@ export function markContainerAsRoot(hostRoot, node) {
30
node[internalContainerInstanceKey] = hostRoot;
31
}
32
33
+export function unmarkContainerAsRoot(node) {
34
+ node[internalContainerInstanceKey] = null;
35
+}
36
+
37
+export function isContainerMarkedAsRoot(node) {
38
+ return !!node[internalContainerInstanceKey];
39
+}
40
+
41
// Given a DOM node, return the closest HostComponent or HostText fiber ancestor.
42
// If the target node is part of a hydrated or not yet rendered subtree, then
43
// this may also return a SuspenseComponent or HostRoot to indicate that.