Ensure createRoot warning parity with ReactDOM.render (#17937)
Dominic Gannaway committed
Jan 30, 2020 at 17:17 UTC
1662035852519983955c8c3bdab72a0c60b1264b
2 files changed
+60
-1
packages/react-dom/src/__tests__/ReactDOMRoot-test.js
+30
@@ -258,4 +258,34 @@ describe('ReactDOMRoot', () => {
258
Scheduler.unstable_flushAll();
259
ReactDOM.createRoot(container); // No warning
260
});
261
+
262
+ it('warns if creating a root on the document.body', async () => {
263
+ expect(() => {
264
+ ReactDOM.createRoot(document.body);
265
+ }).toErrorDev(
266
+ 'createRoot(): Creating roots directly with document.body is ' +
267
+ 'discouraged, since its children are often manipulated by third-party ' +
268
+ 'scripts and browser extensions. This may lead to subtle ' +
269
+ 'reconciliation issues. Try using a container element created ' +
270
+ 'for your app.',
271
+ {withoutStack: true},
272
+ );
273
+ });
274
+
275
+ it('warns if updating a root that has had its contents removed', async () => {
276
+ const root = ReactDOM.createRoot(container);
277
+ root.render(<div>Hi</div>);
278
+ Scheduler.unstable_flushAll();
279
+ container.innerHTML = '';
280
+
281
+ expect(() => {
282
+ root.render(<div>Hi</div>);
283
+ }).toErrorDev(
284
+ 'render(...): It looks like the React-rendered content of the ' +
285
+ 'root container was removed without using React. This is not ' +
286
+ 'supported and will cause errors. Instead, call ' +
287
+ "root.unmount() to empty a root's container.",
288
+ {withoutStack: true},
289
+ );
290
+ });
291
});
packages/react-dom/src/client/ReactDOMRoot.js
+30
-1
@@ -13,6 +13,7 @@ import type {ReactNodeList} from 'shared/ReactTypes';
13
// TODO: This type is shared between the reconciler and ReactDOM, but will
14
// eventually be lifted out to the renderer.
15
import type {FiberRoot} from 'react-reconciler/src/ReactFiberRoot';
16
+import {findHostInstanceWithNoPortals} from 'react-reconciler/inline.dom';
17
18
export type RootType = {
19
render(children: ReactNodeList): void,
@@ -63,6 +64,7 @@ function ReactDOMBlockingRoot(
64
ReactDOMRoot.prototype.render = ReactDOMBlockingRoot.prototype.render = function(
65
children: ReactNodeList,
66
): void {
67
+ const root = this._internalRoot;
68
if (__DEV__) {
69
if (typeof arguments[1] === 'function') {
70
console.error(
@@ -70,8 +72,22 @@ ReactDOMRoot.prototype.render = ReactDOMBlockingRoot.prototype.render = function
72
'To execute a side effect after rendering, declare it in a component body with useEffect().',
73
);
74
}
75
+ const container = root.containerInfo;
76
+
77
+ if (container.nodeType !== COMMENT_NODE) {
78
+ const hostInstance = findHostInstanceWithNoPortals(root.current);
79
+ if (hostInstance) {
80
+ if (hostInstance.parentNode !== container) {
81
+ console.error(
82
+ 'render(...): It looks like the React-rendered content of the ' +
83
+ 'root container was removed without using React. This is not ' +
84
+ 'supported and will cause errors. Instead, call ' +
85
+ "root.unmount() to empty a root's container.",
86
+ );
87
+ }
88
+ }
89
+ }
90
}
74
- const root = this._internalRoot;
91
updateContainer(children, root, null, null);
92
};
93
@@ -156,6 +172,19 @@ export function isValidContainer(node: mixed): boolean {
172
173
function warnIfReactDOMContainerInDEV(container) {
174
if (__DEV__) {
175
+ if (
176
+ container.nodeType === ELEMENT_NODE &&
177
+ ((container: any): Element).tagName &&
178
+ ((container: any): Element).tagName.toUpperCase() === 'BODY'
179
+ ) {
180
+ console.error(
181
+ 'createRoot(): Creating roots directly with document.body is ' +
182
+ 'discouraged, since its children are often manipulated by third-party ' +
183
+ 'scripts and browser extensions. This may lead to subtle ' +
184
+ 'reconciliation issues. Try using a container element created ' +
185
+ 'for your app.',
186
+ );
187
+ }
188
if (isContainerMarkedAsRoot(container)) {
189
if (container._reactRootContainer) {
190
console.error(