From f8062df1d6c6bef5988abe5408d48a357d1021ef Mon Sep 17 00:00:00 2001
From: Dan Abramov
Date: Tue, 1 Aug 2017 21:17:24 +0100
Subject: [PATCH] Add ReactDOM.hydrate() as explicit SSR hydration API (#10339)
* Add ReactDOM.hydrate()
* Deprecate ReactDOM.render() hydration in favor of ReactDOM.hydrate()
* Downgrade the warning level to console.warn()
* Warn when hydrate() is called with empty container
---
.../__tests__/ReactCompositeComponent-test.js | 25 +-
src/renderers/dom/fiber/ReactDOMFiberEntry.js | 57 +-
.../__tests__/ReactDOMComponentTree-test.js | 8 +-
.../ReactDOMServerIntegration-test.js | 50 +-
.../__tests__/ReactDOMTextComponent-test.js | 14 +-
.../dom/shared/__tests__/ReactMount-test.js | 48 +-
.../__tests__/ReactRenderDocument-test.js | 679 ++++++++++++------
.../__tests__/ReactServerRendering-test.js | 157 +++-
.../dom/test/__tests__/ReactTestUtils-test.js | 6 +-
.../shared/server/ReactPartialRenderer.js | 1 -
10 files changed, 770 insertions(+), 275 deletions(-)
diff --git a/src/renderers/__tests__/ReactCompositeComponent-test.js b/src/renderers/__tests__/ReactCompositeComponent-test.js
index 2f69aa9ebcd4b..650157f967f8c 100644
--- a/src/renderers/__tests__/ReactCompositeComponent-test.js
+++ b/src/renderers/__tests__/ReactCompositeComponent-test.js
@@ -15,6 +15,7 @@ var ChildUpdates;
var MorphingComponent;
var React;
var ReactDOM;
+var ReactDOMFeatureFlags;
var ReactDOMServer;
var ReactCurrentOwner;
var ReactTestUtils;
@@ -27,6 +28,7 @@ describe('ReactCompositeComponent', () => {
jest.resetModules();
React = require('react');
ReactDOM = require('react-dom');
+ ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
ReactDOMServer = require('react-dom/server');
ReactCurrentOwner = require('react')
.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner;
@@ -116,11 +118,32 @@ describe('ReactCompositeComponent', () => {
}
}
+ spyOn(console, 'warn');
var markup = ReactDOMServer.renderToString();
+
+ // Old API based on heuristic
var container = document.createElement('div');
container.innerHTML = markup;
-
ReactDOM.render(, container);
+ if (ReactDOMFeatureFlags.useFiber) {
+ expectDev(console.warn.calls.count()).toBe(1);
+ expectDev(console.warn.calls.argsFor(0)[0]).toContain(
+ 'render(): Calling ReactDOM.render() to hydrate server-rendered markup ' +
+ 'will stop working in React v17. Replace the ReactDOM.render() call ' +
+ 'with ReactDOM.hydrate() if you want React to attach to the server HTML.',
+ );
+ } else {
+ expectDev(console.warn.calls.count()).toBe(0);
+ }
+
+ // New explicit API
+ console.warn.calls.reset();
+ if (ReactDOMFeatureFlags.useFiber) {
+ container = document.createElement('div');
+ container.innerHTML = markup;
+ ReactDOM.hydrate(, container);
+ expectDev(console.warn.calls.count()).toBe(0);
+ }
});
it('should react to state changes from callbacks', () => {
diff --git a/src/renderers/dom/fiber/ReactDOMFiberEntry.js b/src/renderers/dom/fiber/ReactDOMFiberEntry.js
index e1b338d53d2cd..c9386768b0848 100644
--- a/src/renderers/dom/fiber/ReactDOMFiberEntry.js
+++ b/src/renderers/dom/fiber/ReactDOMFiberEntry.js
@@ -37,7 +37,7 @@ var {
DOCUMENT_NODE,
DOCUMENT_FRAGMENT_NODE,
} = require('HTMLNodeType');
-var {ID_ATTRIBUTE_NAME} = require('DOMProperty');
+var {ROOT_ATTRIBUTE_NAME} = require('DOMProperty');
var findDOMNode = require('findDOMNode');
var invariant = require('fbjs/lib/invariant');
@@ -58,6 +58,7 @@ var {
var {precacheFiberNode, updateFiberProps} = ReactDOMComponentTree;
if (__DEV__) {
+ var lowPriorityWarning = require('lowPriorityWarning');
var warning = require('fbjs/lib/warning');
var validateDOMNesting = require('validateDOMNesting');
var {updatedAncestorInfo} = validateDOMNesting;
@@ -127,11 +128,11 @@ function getReactRootElementInContainer(container: any) {
}
}
-function shouldReuseContent(container) {
+function shouldHydrateDueToLegacyHeuristic(container) {
const rootElement = getReactRootElementInContainer(container);
return !!(rootElement &&
rootElement.nodeType === ELEMENT_NODE &&
- rootElement.hasAttribute(ID_ATTRIBUTE_NAME));
+ rootElement.hasAttribute(ROOT_ATTRIBUTE_NAME));
}
function shouldAutoFocusHostComponent(type: string, props: Props): boolean {
@@ -523,10 +524,14 @@ ReactGenericBatching.injection.injectFiberBatchedUpdates(
DOMRenderer.batchedUpdates,
);
+var warnedAboutHydrateAPI = false;
+var warnedAboutEmptyContainer = false;
+
function renderSubtreeIntoContainer(
parentComponent: ?ReactComponent,
children: ReactNodeList,
container: DOMContainer,
+ forceHydrate: boolean,
callback: ?Function,
) {
invariant(
@@ -577,9 +582,10 @@ function renderSubtreeIntoContainer(
let root = container._reactRootContainer;
if (!root) {
+ const shouldHydrate =
+ forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
// First clear any existing content.
- // TODO: Figure out the best heuristic here.
- if (!shouldReuseContent(container)) {
+ if (!shouldHydrate) {
let warned = false;
let rootSibling;
while ((rootSibling = container.lastChild)) {
@@ -587,7 +593,7 @@ function renderSubtreeIntoContainer(
if (
!warned &&
rootSibling.nodeType === ELEMENT_NODE &&
- (rootSibling: any).hasAttribute(ID_ATTRIBUTE_NAME)
+ (rootSibling: any).hasAttribute(ROOT_ATTRIBUTE_NAME)
) {
warned = true;
warning(
@@ -601,6 +607,25 @@ function renderSubtreeIntoContainer(
container.removeChild(rootSibling);
}
}
+ if (__DEV__) {
+ if (shouldHydrate && !forceHydrate && !warnedAboutHydrateAPI) {
+ warnedAboutHydrateAPI = true;
+ lowPriorityWarning(
+ false,
+ 'render(): Calling ReactDOM.render() to hydrate server-rendered markup ' +
+ 'will stop working in React v17. Replace the ReactDOM.render() call ' +
+ 'with ReactDOM.hydrate() if you want React to attach to the server HTML.',
+ );
+ }
+ if (forceHydrate && !container.firstChild && !warnedAboutEmptyContainer) {
+ warnedAboutEmptyContainer = true;
+ warning(
+ false,
+ 'hydrate(): Expected to hydrate from server-rendered markup, but the passed ' +
+ 'DOM container node was empty. React will create the DOM from scratch.',
+ );
+ }
+ }
const newRoot = DOMRenderer.createContainer(container);
root = container._reactRootContainer = newRoot;
// Initial mount should not be batched.
@@ -614,6 +639,15 @@ function renderSubtreeIntoContainer(
}
var ReactDOMFiber = {
+ hydrate(
+ element: ReactElement,
+ container: DOMContainer,
+ callback: ?Function,
+ ) {
+ // TODO: throw or warn if we couldn't hydrate?
+ return renderSubtreeIntoContainer(null, element, container, true, callback);
+ },
+
render(
element: ReactElement,
container: DOMContainer,
@@ -651,7 +685,13 @@ var ReactDOMFiber = {
}
}
}
- return renderSubtreeIntoContainer(null, element, container, callback);
+ return renderSubtreeIntoContainer(
+ null,
+ element,
+ container,
+ false,
+ callback,
+ );
},
unstable_renderSubtreeIntoContainer(
@@ -668,6 +708,7 @@ var ReactDOMFiber = {
parentComponent,
element,
containerNode,
+ false,
callback,
);
},
@@ -692,7 +733,7 @@ var ReactDOMFiber = {
// Unmount should not be batched.
DOMRenderer.unbatchedUpdates(() => {
- renderSubtreeIntoContainer(null, null, container, () => {
+ renderSubtreeIntoContainer(null, null, container, false, () => {
container._reactRootContainer = null;
});
});
diff --git a/src/renderers/dom/shared/__tests__/ReactDOMComponentTree-test.js b/src/renderers/dom/shared/__tests__/ReactDOMComponentTree-test.js
index 369420522bb45..fd1b463d5f0e7 100644
--- a/src/renderers/dom/shared/__tests__/ReactDOMComponentTree-test.js
+++ b/src/renderers/dom/shared/__tests__/ReactDOMComponentTree-test.js
@@ -11,6 +11,8 @@
'use strict';
+var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
+
describe('ReactDOMComponentTree', () => {
var React;
var ReactDOM;
@@ -21,7 +23,11 @@ describe('ReactDOMComponentTree', () => {
var container = document.createElement('div');
// Force server-rendering path:
container.innerHTML = ReactDOMServer.renderToString(elt);
- return ReactDOM.render(elt, container);
+ if (ReactDOMFeatureFlags.useFiber) {
+ return ReactDOM.hydrate(elt, container);
+ } else {
+ return ReactDOM.render(elt, container);
+ }
}
function getTypeOf(instance) {
diff --git a/src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js b/src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js
index 6c7aa87bc5465..8654506b56282 100644
--- a/src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js
+++ b/src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js
@@ -30,9 +30,13 @@ const COMMENT_NODE_TYPE = 8;
// ====================================
// promisified version of ReactDOM.render()
-function asyncReactDOMRender(reactElement, domElement) {
+function asyncReactDOMRender(reactElement, domElement, forceHydrate) {
return new Promise(resolve => {
- ReactDOM.render(reactElement, domElement);
+ if (forceHydrate && ReactDOMFeatureFlags.useFiber) {
+ ReactDOM.hydrate(reactElement, domElement);
+ } else {
+ ReactDOM.render(reactElement, domElement);
+ }
// We can't use the callback for resolution because that will not catch
// errors. They're thrown.
resolve();
@@ -68,10 +72,10 @@ async function expectErrors(fn, count) {
// renders the reactElement into domElement, and expects a certain number of errors.
// returns a Promise that resolves when the render is complete.
-function renderIntoDom(reactElement, domElement, errorCount = 0) {
+function renderIntoDom(reactElement, domElement, forceHydrate, errorCount = 0) {
return expectErrors(async () => {
ExecutionEnvironment.canUseDOM = true;
- await asyncReactDOMRender(reactElement, domElement);
+ await asyncReactDOMRender(reactElement, domElement, forceHydrate);
ExecutionEnvironment.canUseDOM = false;
return domElement.firstChild;
}, errorCount);
@@ -135,7 +139,7 @@ async function streamRender(reactElement, errorCount = 0) {
const clientCleanRender = (element, errorCount = 0) => {
const div = document.createElement('div');
- return renderIntoDom(element, div, errorCount);
+ return renderIntoDom(element, div, false, errorCount);
};
const clientRenderOnServerString = async (element, errorCount = 0) => {
@@ -146,7 +150,12 @@ const clientRenderOnServerString = async (element, errorCount = 0) => {
domElement.innerHTML = markup;
let serverNode = domElement.firstChild;
- const firstClientNode = await renderIntoDom(element, domElement, errorCount);
+ const firstClientNode = await renderIntoDom(
+ element,
+ domElement,
+ true,
+ errorCount,
+ );
let clientNode = firstClientNode;
// Make sure all top level nodes match up
@@ -154,19 +163,10 @@ const clientRenderOnServerString = async (element, errorCount = 0) => {
expect(serverNode != null).toBe(true);
expect(clientNode != null).toBe(true);
expect(clientNode.nodeType).toBe(serverNode.nodeType);
- if (clientNode.nodeType === TEXT_NODE_TYPE) {
- // Text nodes are stateless so we can just compare values.
- // This works around a current issue where hydration replaces top-level
- // text node, but otherwise works.
- // TODO: we can remove this branch if we add explicit hydration opt-in.
- // https://github.com/facebook/react/issues/10189
- expect(serverNode.nodeValue).toBe(clientNode.nodeValue);
- } else {
- // Assert that the DOM element hasn't been replaced.
- // Note that we cannot use expect(serverNode).toBe(clientNode) because
- // of jest bug #1772.
- expect(serverNode === clientNode).toBe(true);
- }
+ // Assert that the DOM element hasn't been replaced.
+ // Note that we cannot use expect(serverNode).toBe(clientNode) because
+ // of jest bug #1772.
+ expect(serverNode === clientNode).toBe(true);
serverNode = serverNode.nextSibling;
clientNode = clientNode.nextSibling;
}
@@ -180,7 +180,7 @@ const clientRenderOnBadMarkup = async (element, errorCount = 0) => {
var domElement = document.createElement('div');
domElement.innerHTML =
'';
- await renderIntoDom(element, domElement, errorCount + 1);
+ await renderIntoDom(element, domElement, true, errorCount + 1);
// This gives us the resulting text content.
var hydratedTextContent = domElement.textContent;
@@ -188,7 +188,7 @@ const clientRenderOnBadMarkup = async (element, errorCount = 0) => {
// Next we render the element into a clean DOM node client side.
const cleanDomElement = document.createElement('div');
ExecutionEnvironment.canUseDOM = true;
- await asyncReactDOMRender(element, cleanDomElement);
+ await asyncReactDOMRender(element, cleanDomElement, true);
ExecutionEnvironment.canUseDOM = false;
// This gives us the expected text content.
const cleanTextContent = cleanDomElement.textContent;
@@ -296,6 +296,7 @@ async function testMarkupMatch(serverElement, clientElement, shouldMatch) {
return renderIntoDom(
clientElement,
domElement.parentNode,
+ true,
shouldMatch ? 0 : 1,
);
}
@@ -1971,7 +1972,11 @@ describe('ReactDOMServerIntegration', () => {
resetModules();
// client render on top of the server markup.
- const clientField = await renderIntoDom(element, field.parentNode);
+ const clientField = await renderIntoDom(
+ element,
+ field.parentNode,
+ true,
+ );
// verify that the input field was not replaced.
// Note that we cannot use expect(clientField).toBe(field) because
// of jest bug #1772
@@ -2330,6 +2335,7 @@ describe('ReactDOMServerIntegration', () => {
await asyncReactDOMRender(
(component = e)} />,
root,
+ true,
);
expect(component.refs.myDiv).toBe(root.firstChild);
});
diff --git a/src/renderers/dom/shared/__tests__/ReactDOMTextComponent-test.js b/src/renderers/dom/shared/__tests__/ReactDOMTextComponent-test.js
index 013eabaf04a1b..28a2196a0209f 100644
--- a/src/renderers/dom/shared/__tests__/ReactDOMTextComponent-test.js
+++ b/src/renderers/dom/shared/__tests__/ReactDOMTextComponent-test.js
@@ -14,6 +14,7 @@
var React;
var ReactDOM;
var ReactDOMServer;
+var ReactDOMFeatureFlags;
// In standard React, TextComponent keeps track of different Text templates
// using comments. However, in React Fiber, those comments are not outputted due
@@ -29,6 +30,7 @@ describe('ReactDOMTextComponent', () => {
React = require('react');
ReactDOM = require('react-dom');
ReactDOMServer = require('react-dom/server');
+ ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
});
it('updates a mounted text component in place', () => {
@@ -117,7 +119,11 @@ describe('ReactDOMTextComponent', () => {
var reactEl = {'foo'}{'bar'}{'baz'}
;
el.innerHTML = ReactDOMServer.renderToString(reactEl);
- ReactDOM.render(reactEl, el);
+ if (ReactDOMFeatureFlags.useFiber) {
+ ReactDOM.hydrate(reactEl, el);
+ } else {
+ ReactDOM.render(reactEl, el);
+ }
expect(el.textContent).toBe('foobarbaz');
ReactDOM.unmountComponentAtNode(el);
@@ -125,7 +131,11 @@ describe('ReactDOMTextComponent', () => {
reactEl = {''}{''}{''}
;
el.innerHTML = ReactDOMServer.renderToString(reactEl);
- ReactDOM.render(reactEl, el);
+ if (ReactDOMFeatureFlags.useFiber) {
+ ReactDOM.hydrate(reactEl, el);
+ } else {
+ ReactDOM.render(reactEl, el);
+ }
expect(el.textContent).toBe('');
});
diff --git a/src/renderers/dom/shared/__tests__/ReactMount-test.js b/src/renderers/dom/shared/__tests__/ReactMount-test.js
index a522675d74e10..458cbd09ff909 100644
--- a/src/renderers/dom/shared/__tests__/ReactMount-test.js
+++ b/src/renderers/dom/shared/__tests__/ReactMount-test.js
@@ -146,12 +146,16 @@ describe('ReactMount', () => {
expect(instance1 === instance2).toBe(true);
});
- it('should warn if mounting into dirty rendered markup', () => {
+ it('should warn if mounting into left padded rendered markup', () => {
var container = document.createElement('container');
container.innerHTML = ReactDOMServer.renderToString() + ' ';
spyOn(console, 'error');
- ReactDOM.render(, container);
+ if (ReactDOMFeatureFlags.useFiber) {
+ ReactDOM.hydrate(, container);
+ } else {
+ ReactDOM.render(, container);
+ }
expectDev(console.error.calls.count()).toBe(1);
if (ReactDOMFeatureFlags.useFiber) {
expectDev(console.error.calls.argsFor(0)[0]).toContain(
@@ -162,15 +166,28 @@ describe('ReactMount', () => {
'Target node has markup rendered by React, but there are unrelated nodes as well.',
);
}
+ });
- console.error.calls.reset();
- ReactDOM.unmountComponentAtNode(container);
+ it('should warn if mounting into right padded rendered markup', () => {
+ var container = document.createElement('container');
container.innerHTML = ' ' + ReactDOMServer.renderToString();
- ReactDOM.render(, container);
+
+ spyOn(console, 'error');
+ if (ReactDOMFeatureFlags.useFiber) {
+ ReactDOM.hydrate(, container);
+ } else {
+ ReactDOM.render(, container);
+ }
expectDev(console.error.calls.count()).toBe(1);
- expectDev(console.error.calls.argsFor(0)[0]).toContain(
- 'Target node has markup rendered by React, but there are unrelated nodes as well.',
- );
+ if (ReactDOMFeatureFlags.useFiber) {
+ expectDev(console.error.calls.argsFor(0)[0]).toContain(
+ 'Did not expect server HTML to contain the text node " " in .',
+ );
+ } else {
+ expectDev(console.error.calls.argsFor(0)[0]).toContain(
+ 'Target node has markup rendered by React, but there are unrelated nodes as well.',
+ );
+ }
});
it('should not warn if mounting into non-empty node', () => {
@@ -203,10 +220,17 @@ describe('ReactMount', () => {
div.innerHTML = markup;
spyOn(console, 'error');
- ReactDOM.render(
- This markup contains an nbsp entity: client text
,
- div,
- );
+ if (ReactDOMFeatureFlags.useFiber) {
+ ReactDOM.hydrate(
+ This markup contains an nbsp entity: client text
,
+ div,
+ );
+ } else {
+ ReactDOM.render(
+ This markup contains an nbsp entity: client text
,
+ div,
+ );
+ }
expectDev(console.error.calls.count()).toBe(1);
if (ReactDOMFeatureFlags.useFiber) {
expectDev(console.error.calls.argsFor(0)[0]).toContain(
diff --git a/src/renderers/dom/shared/__tests__/ReactRenderDocument-test.js b/src/renderers/dom/shared/__tests__/ReactRenderDocument-test.js
index 21089d763821d..89eccba3bc25c 100644
--- a/src/renderers/dom/shared/__tests__/ReactRenderDocument-test.js
+++ b/src/renderers/dom/shared/__tests__/ReactRenderDocument-test.js
@@ -14,10 +14,11 @@
var React;
var ReactDOM;
var ReactDOMServer;
-var ReactDOMFeatureFlags;
var getTestDocument;
+var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
+
var UNMOUNT_INVARIANT_MESSAGE =
' tried to unmount. ' +
'Because of cross-browser quirks it is impossible to unmount some ' +
@@ -32,256 +33,524 @@ describe('rendering React components at document', () => {
React = require('react');
ReactDOM = require('react-dom');
ReactDOMServer = require('react-dom/server');
- ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
getTestDocument = require('getTestDocument');
});
- it('should be able to adopt server markup', () => {
- class Root extends React.Component {
- render() {
- return (
-
-
- Hello World
-
-
- {'Hello ' + this.props.hello}
-
-
+ describe('with old implicit hydration API', () => {
+ function expectDeprecationWarningWithFiber() {
+ if (ReactDOMFeatureFlags.useFiber) {
+ expectDev(console.warn.calls.count()).toBe(1);
+ expectDev(console.warn.calls.argsFor(0)[0]).toContain(
+ 'render(): Calling ReactDOM.render() to hydrate server-rendered markup ' +
+ 'will stop working in React v17. Replace the ReactDOM.render() call ' +
+ 'with ReactDOM.hydrate() if you want React to attach to the server HTML.',
);
+ } else {
+ expectDev(console.warn.calls.count()).toBe(0);
}
}
- var markup = ReactDOMServer.renderToString();
- var testDocument = getTestDocument(markup);
- var body = testDocument.body;
-
- ReactDOM.render(, testDocument);
- expect(testDocument.body.innerHTML).toBe('Hello world');
+ it('should be able to adopt server markup', () => {
+ spyOn(console, 'warn');
+ class Root extends React.Component {
+ render() {
+ return (
+
+
+ Hello World
+
+
+ {'Hello ' + this.props.hello}
+
+
+ );
+ }
+ }
- ReactDOM.render(, testDocument);
- expect(testDocument.body.innerHTML).toBe('Hello moon');
+ var markup = ReactDOMServer.renderToString();
+ var testDocument = getTestDocument(markup);
+ var body = testDocument.body;
- expect(body === testDocument.body).toBe(true);
- });
+ ReactDOM.render(, testDocument);
+ expect(testDocument.body.innerHTML).toBe('Hello world');
- it('should not be able to unmount component from document node', () => {
- class Root extends React.Component {
- render() {
- return (
-
-
- Hello World
-
-
- Hello world
-
-
- );
+ ReactDOM.render(, testDocument);
+ expect(testDocument.body.innerHTML).toBe('Hello moon');
+
+ expect(body === testDocument.body).toBe(true);
+ expectDeprecationWarningWithFiber();
+ });
+
+ it('should not be able to unmount component from document node', () => {
+ spyOn(console, 'warn');
+ class Root extends React.Component {
+ render() {
+ return (
+
+
+ Hello World
+
+
+ Hello world
+
+
+ );
+ }
}
- }
-
- var markup = ReactDOMServer.renderToString();
- var testDocument = getTestDocument(markup);
- ReactDOM.render(, testDocument);
- expect(testDocument.body.innerHTML).toBe('Hello world');
-
- if (ReactDOMFeatureFlags.useFiber) {
- // In Fiber this actually works. It might not be a good idea though.
- ReactDOM.unmountComponentAtNode(testDocument);
- expect(testDocument.firstChild).toBe(null);
- } else {
- expect(function() {
- ReactDOM.unmountComponentAtNode(testDocument);
- }).toThrowError(UNMOUNT_INVARIANT_MESSAGE);
+ var markup = ReactDOMServer.renderToString();
+ var testDocument = getTestDocument(markup);
+ ReactDOM.render(, testDocument);
expect(testDocument.body.innerHTML).toBe('Hello world');
- }
- });
- it('should not be able to switch root constructors', () => {
- class Component extends React.Component {
- render() {
- return (
-
-
- Hello World
-
-
- Hello world
-
-
- );
+ if (ReactDOMFeatureFlags.useFiber) {
+ // In Fiber this actually works. It might not be a good idea though.
+ ReactDOM.unmountComponentAtNode(testDocument);
+ expect(testDocument.firstChild).toBe(null);
+ } else {
+ expect(function() {
+ ReactDOM.unmountComponentAtNode(testDocument);
+ }).toThrowError(UNMOUNT_INVARIANT_MESSAGE);
+
+ expect(testDocument.body.innerHTML).toBe('Hello world');
}
- }
- class Component2 extends React.Component {
- render() {
- return (
-
-
- Hello World
-
-
- Goodbye world
-
-
- );
+ expectDeprecationWarningWithFiber();
+ });
+
+ it('should not be able to switch root constructors', () => {
+ spyOn(console, 'warn');
+ class Component extends React.Component {
+ render() {
+ return (
+
+
+ Hello World
+
+
+ Hello world
+
+
+ );
+ }
}
- }
- var markup = ReactDOMServer.renderToString();
- var testDocument = getTestDocument(markup);
+ class Component2 extends React.Component {
+ render() {
+ return (
+
+
+ Hello World
+
+
+ Goodbye world
+
+
+ );
+ }
+ }
- ReactDOM.render(, testDocument);
+ var markup = ReactDOMServer.renderToString();
+ var testDocument = getTestDocument(markup);
- expect(testDocument.body.innerHTML).toBe('Hello world');
+ ReactDOM.render(, testDocument);
- // Reactive update
- if (ReactDOMFeatureFlags.useFiber) {
- // This works but is probably a bad idea.
- ReactDOM.render(, testDocument);
+ expect(testDocument.body.innerHTML).toBe('Hello world');
- expect(testDocument.body.innerHTML).toBe('Goodbye world');
- } else {
- expect(function() {
+ // Reactive update
+ if (ReactDOMFeatureFlags.useFiber) {
+ // This works but is probably a bad idea.
ReactDOM.render(, testDocument);
- }).toThrowError(UNMOUNT_INVARIANT_MESSAGE);
- expect(testDocument.body.innerHTML).toBe('Hello world');
- }
- });
+ expect(testDocument.body.innerHTML).toBe('Goodbye world');
+ } else {
+ expect(function() {
+ ReactDOM.render(, testDocument);
+ }).toThrowError(UNMOUNT_INVARIANT_MESSAGE);
- it('should be able to mount into document', () => {
- class Component extends React.Component {
- render() {
- return (
-
-
- Hello World
-
-
- {this.props.text}
-
-
- );
+ expect(testDocument.body.innerHTML).toBe('Hello world');
}
- }
- var markup = ReactDOMServer.renderToString(
- ,
- );
- var testDocument = getTestDocument(markup);
+ expectDeprecationWarningWithFiber();
+ });
+
+ it('should be able to mount into document', () => {
+ spyOn(console, 'warn');
+ class Component extends React.Component {
+ render() {
+ return (
+
+
+ Hello World
+
+
+ {this.props.text}
+
+
+ );
+ }
+ }
- ReactDOM.render(, testDocument);
+ var markup = ReactDOMServer.renderToString(
+ ,
+ );
+ var testDocument = getTestDocument(markup);
- expect(testDocument.body.innerHTML).toBe('Hello world');
- });
+ ReactDOM.render(, testDocument);
- it('renders over an existing text child without throwing', () => {
- const container = document.createElement('div');
- container.textContent = 'potato';
- ReactDOM.render(parsnip
, container);
- expect(container.textContent).toBe('parsnip');
- });
+ expect(testDocument.body.innerHTML).toBe('Hello world');
+ expectDeprecationWarningWithFiber();
+ });
+
+ it('renders over an existing text child without throwing', () => {
+ const container = document.createElement('div');
+ container.textContent = 'potato';
+ ReactDOM.render(parsnip
, container);
+ expect(container.textContent).toBe('parsnip');
+ // We don't expect a warning about new hydration API here because
+ // we aren't sure if the user meant to hydrate or replace a stub node.
+ // We would see a warning if the container had React-rendered HTML in it.
+ });
+
+ it('should give helpful errors on state desync', () => {
+ class Component extends React.Component {
+ render() {
+ return (
+
+
+ Hello World
+
+
+ {this.props.text}
+
+
+ );
+ }
+ }
- it('should give helpful errors on state desync', () => {
- class Component extends React.Component {
- render() {
- return (
-
-
- Hello World
-
-
- {this.props.text}
-
-
+ var markup = ReactDOMServer.renderToString(
+ ,
+ );
+ var testDocument = getTestDocument(markup);
+
+ if (ReactDOMFeatureFlags.useFiber) {
+ spyOn(console, 'warn');
+ spyOn(console, 'error');
+ ReactDOM.render(, testDocument);
+ expect(testDocument.body.innerHTML).toBe('Hello world');
+ expectDev(console.warn.calls.count()).toBe(1);
+ expectDev(console.warn.calls.argsFor(0)[0]).toContain(
+ 'render(): Calling ReactDOM.render() to hydrate server-rendered markup ' +
+ 'will stop working in React v17. Replace the ReactDOM.render() call ' +
+ 'with ReactDOM.hydrate() if you want React to attach to the server HTML.',
+ );
+ expectDev(console.error.calls.count()).toBe(1);
+ expectDev(console.error.calls.argsFor(0)[0]).toContain(
+ 'Warning: Text content did not match.',
+ );
+ } else {
+ expect(function() {
+ // Notice the text is different!
+ ReactDOM.render(, testDocument);
+ }).toThrowError(
+ "You're trying to render a component to the document using " +
+ 'server rendering but the checksum was invalid. This usually ' +
+ 'means you rendered a different component type or props on ' +
+ 'the client from the one on the server, or your render() methods ' +
+ 'are impure. React cannot handle this case due to cross-browser ' +
+ 'quirks by rendering at the document root. You should look for ' +
+ 'environment dependent code in your components and ensure ' +
+ 'the props are the same client and server side:\n' +
+ ' (client) dy data-reactid="4">Hello world\n' +
+ ' (server) dy data-reactid="4">Goodbye world
+ {this.props.text}
+ ',
);
}
- }
-
- var markup = ReactDOMServer.renderToString(
- ,
- );
- var testDocument = getTestDocument(markup);
+ });
+
+ it('should throw on full document render w/ no markup', () => {
+ var testDocument = getTestDocument();
+
+ class Component extends React.Component {
+ render() {
+ return (
+
+