@samitouri / QOS-React / commits / f8062df1d6

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

Dan Abramov committed Aug 1, 2017 at 21:17 UTC f8062df1d6c6bef5988abe5408d48a357d1021ef
10 files changed +770 -275
src/renderers/__tests__/ReactCompositeComponent-test.js
+24 -1
@@ -15,6 +15,7 @@ var ChildUpdates;
15 var MorphingComponent;
16 var React;
17 var ReactDOM;
18 +var ReactDOMFeatureFlags;
19 var ReactDOMServer;
20 var ReactCurrentOwner;
21 var ReactTestUtils;
@@ -27,6 +28,7 @@ describe('ReactCompositeComponent', () => {
28 jest.resetModules();
29 React = require('react');
30 ReactDOM = require('react-dom');
31 + ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
32 ReactDOMServer = require('react-dom/server');
33 ReactCurrentOwner = require('react')
34 .__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner;
@@ -116,11 +118,32 @@ describe('ReactCompositeComponent', () => {
118 }
119 }
120
121 + spyOn(console, 'warn');
122 var markup = ReactDOMServer.renderToString(<Parent />);
123 +
124 + // Old API based on heuristic
125 var container = document.createElement('div');
126 container.innerHTML = markup;
122 -
127 ReactDOM.render(<Parent />, container);
128 + if (ReactDOMFeatureFlags.useFiber) {
129 + expectDev(console.warn.calls.count()).toBe(1);
130 + expectDev(console.warn.calls.argsFor(0)[0]).toContain(
131 + 'render(): Calling ReactDOM.render() to hydrate server-rendered markup ' +
132 + 'will stop working in React v17. Replace the ReactDOM.render() call ' +
133 + 'with ReactDOM.hydrate() if you want React to attach to the server HTML.',
134 + );
135 + } else {
136 + expectDev(console.warn.calls.count()).toBe(0);
137 + }
138 +
139 + // New explicit API
140 + console.warn.calls.reset();
141 + if (ReactDOMFeatureFlags.useFiber) {
142 + container = document.createElement('div');
143 + container.innerHTML = markup;
144 + ReactDOM.hydrate(<Parent />, container);
145 + expectDev(console.warn.calls.count()).toBe(0);
146 + }
147 });
148
149 it('should react to state changes from callbacks', () => {
src/renderers/dom/fiber/ReactDOMFiberEntry.js
+49 -8
@@ -37,7 +37,7 @@ var {
37 DOCUMENT_NODE,
38 DOCUMENT_FRAGMENT_NODE,
39 } = require('HTMLNodeType');
40 -var {ID_ATTRIBUTE_NAME} = require('DOMProperty');
40 +var {ROOT_ATTRIBUTE_NAME} = require('DOMProperty');
41
42 var findDOMNode = require('findDOMNode');
43 var invariant = require('fbjs/lib/invariant');
@@ -58,6 +58,7 @@ var {
58 var {precacheFiberNode, updateFiberProps} = ReactDOMComponentTree;
59
60 if (__DEV__) {
61 + var lowPriorityWarning = require('lowPriorityWarning');
62 var warning = require('fbjs/lib/warning');
63 var validateDOMNesting = require('validateDOMNesting');
64 var {updatedAncestorInfo} = validateDOMNesting;
@@ -127,11 +128,11 @@ function getReactRootElementInContainer(container: any) {
128 }
129 }
130
130 -function shouldReuseContent(container) {
131 +function shouldHydrateDueToLegacyHeuristic(container) {
132 const rootElement = getReactRootElementInContainer(container);
133 return !!(rootElement &&
134 rootElement.nodeType === ELEMENT_NODE &&
134 - rootElement.hasAttribute(ID_ATTRIBUTE_NAME));
135 + rootElement.hasAttribute(ROOT_ATTRIBUTE_NAME));
136 }
137
138 function shouldAutoFocusHostComponent(type: string, props: Props): boolean {
@@ -523,10 +524,14 @@ ReactGenericBatching.injection.injectFiberBatchedUpdates(
524 DOMRenderer.batchedUpdates,
525 );
526
527 +var warnedAboutHydrateAPI = false;
528 +var warnedAboutEmptyContainer = false;
529 +
530 function renderSubtreeIntoContainer(
531 parentComponent: ?ReactComponent<any, any, any>,
532 children: ReactNodeList,
533 container: DOMContainer,
534 + forceHydrate: boolean,
535 callback: ?Function,
536 ) {
537 invariant(
@@ -577,9 +582,10 @@ function renderSubtreeIntoContainer(
582
583 let root = container._reactRootContainer;
584 if (!root) {
585 + const shouldHydrate =
586 + forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
587 // First clear any existing content.
581 - // TODO: Figure out the best heuristic here.
582 - if (!shouldReuseContent(container)) {
588 + if (!shouldHydrate) {
589 let warned = false;
590 let rootSibling;
591 while ((rootSibling = container.lastChild)) {
@@ -587,7 +593,7 @@ function renderSubtreeIntoContainer(
593 if (
594 !warned &&
595 rootSibling.nodeType === ELEMENT_NODE &&
590 - (rootSibling: any).hasAttribute(ID_ATTRIBUTE_NAME)
596 + (rootSibling: any).hasAttribute(ROOT_ATTRIBUTE_NAME)
597 ) {
598 warned = true;
599 warning(
@@ -601,6 +607,25 @@ function renderSubtreeIntoContainer(
607 container.removeChild(rootSibling);
608 }
609 }
610 + if (__DEV__) {
611 + if (shouldHydrate && !forceHydrate && !warnedAboutHydrateAPI) {
612 + warnedAboutHydrateAPI = true;
613 + lowPriorityWarning(
614 + false,
615 + 'render(): Calling ReactDOM.render() to hydrate server-rendered markup ' +
616 + 'will stop working in React v17. Replace the ReactDOM.render() call ' +
617 + 'with ReactDOM.hydrate() if you want React to attach to the server HTML.',
618 + );
619 + }
620 + if (forceHydrate && !container.firstChild && !warnedAboutEmptyContainer) {
621 + warnedAboutEmptyContainer = true;
622 + warning(
623 + false,
624 + 'hydrate(): Expected to hydrate from server-rendered markup, but the passed ' +
625 + 'DOM container node was empty. React will create the DOM from scratch.',
626 + );
627 + }
628 + }
629 const newRoot = DOMRenderer.createContainer(container);
630 root = container._reactRootContainer = newRoot;
631 // Initial mount should not be batched.
@@ -614,6 +639,15 @@ function renderSubtreeIntoContainer(
639 }
640
641 var ReactDOMFiber = {
642 + hydrate(
643 + element: ReactElement<any>,
644 + container: DOMContainer,
645 + callback: ?Function,
646 + ) {
647 + // TODO: throw or warn if we couldn't hydrate?
648 + return renderSubtreeIntoContainer(null, element, container, true, callback);
649 + },
650 +
651 render(
652 element: ReactElement<any>,
653 container: DOMContainer,
@@ -651,7 +685,13 @@ var ReactDOMFiber = {
685 }
686 }
687 }
654 - return renderSubtreeIntoContainer(null, element, container, callback);
688 + return renderSubtreeIntoContainer(
689 + null,
690 + element,
691 + container,
692 + false,
693 + callback,
694 + );
695 },
696
697 unstable_renderSubtreeIntoContainer(
@@ -668,6 +708,7 @@ var ReactDOMFiber = {
708 parentComponent,
709 element,
710 containerNode,
711 + false,
712 callback,
713 );
714 },
@@ -692,7 +733,7 @@ var ReactDOMFiber = {
733
734 // Unmount should not be batched.
735 DOMRenderer.unbatchedUpdates(() => {
695 - renderSubtreeIntoContainer(null, null, container, () => {
736 + renderSubtreeIntoContainer(null, null, container, false, () => {
737 container._reactRootContainer = null;
738 });
739 });
src/renderers/dom/shared/__tests__/ReactDOMComponentTree-test.js
+7 -1
@@ -11,6 +11,8 @@
11
12 'use strict';
13
14 +var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
15 +
16 describe('ReactDOMComponentTree', () => {
17 var React;
18 var ReactDOM;
@@ -21,7 +23,11 @@ describe('ReactDOMComponentTree', () => {
23 var container = document.createElement('div');
24 // Force server-rendering path:
25 container.innerHTML = ReactDOMServer.renderToString(elt);
24 - return ReactDOM.render(elt, container);
26 + if (ReactDOMFeatureFlags.useFiber) {
27 + return ReactDOM.hydrate(elt, container);
28 + } else {
29 + return ReactDOM.render(elt, container);
30 + }
31 }
32
33 function getTypeOf(instance) {
src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js
+28 -22
@@ -30,9 +30,13 @@ const COMMENT_NODE_TYPE = 8;
30 // ====================================
31
32 // promisified version of ReactDOM.render()
33 -function asyncReactDOMRender(reactElement, domElement) {
33 +function asyncReactDOMRender(reactElement, domElement, forceHydrate) {
34 return new Promise(resolve => {
35 - ReactDOM.render(reactElement, domElement);
35 + if (forceHydrate && ReactDOMFeatureFlags.useFiber) {
36 + ReactDOM.hydrate(reactElement, domElement);
37 + } else {
38 + ReactDOM.render(reactElement, domElement);
39 + }
40 // We can't use the callback for resolution because that will not catch
41 // errors. They're thrown.
42 resolve();
@@ -68,10 +72,10 @@ async function expectErrors(fn, count) {
72
73 // renders the reactElement into domElement, and expects a certain number of errors.
74 // returns a Promise that resolves when the render is complete.
71 -function renderIntoDom(reactElement, domElement, errorCount = 0) {
75 +function renderIntoDom(reactElement, domElement, forceHydrate, errorCount = 0) {
76 return expectErrors(async () => {
77 ExecutionEnvironment.canUseDOM = true;
74 - await asyncReactDOMRender(reactElement, domElement);
78 + await asyncReactDOMRender(reactElement, domElement, forceHydrate);
79 ExecutionEnvironment.canUseDOM = false;
80 return domElement.firstChild;
81 }, errorCount);
@@ -135,7 +139,7 @@ async function streamRender(reactElement, errorCount = 0) {
139
140 const clientCleanRender = (element, errorCount = 0) => {
141 const div = document.createElement('div');
138 - return renderIntoDom(element, div, errorCount);
142 + return renderIntoDom(element, div, false, errorCount);
143 };
144
145 const clientRenderOnServerString = async (element, errorCount = 0) => {
@@ -146,7 +150,12 @@ const clientRenderOnServerString = async (element, errorCount = 0) => {
150 domElement.innerHTML = markup;
151 let serverNode = domElement.firstChild;
152
149 - const firstClientNode = await renderIntoDom(element, domElement, errorCount);
153 + const firstClientNode = await renderIntoDom(
154 + element,
155 + domElement,
156 + true,
157 + errorCount,
158 + );
159 let clientNode = firstClientNode;
160
161 // Make sure all top level nodes match up
@@ -154,19 +163,10 @@ const clientRenderOnServerString = async (element, errorCount = 0) => {
163 expect(serverNode != null).toBe(true);
164 expect(clientNode != null).toBe(true);
165 expect(clientNode.nodeType).toBe(serverNode.nodeType);
157 - if (clientNode.nodeType === TEXT_NODE_TYPE) {
158 - // Text nodes are stateless so we can just compare values.
159 - // This works around a current issue where hydration replaces top-level
160 - // text node, but otherwise works.
161 - // TODO: we can remove this branch if we add explicit hydration opt-in.
162 - // https://github.com/facebook/react/issues/10189
163 - expect(serverNode.nodeValue).toBe(clientNode.nodeValue);
164 - } else {
165 - // Assert that the DOM element hasn't been replaced.
166 - // Note that we cannot use expect(serverNode).toBe(clientNode) because
167 - // of jest bug #1772.
168 - expect(serverNode === clientNode).toBe(true);
169 - }
166 + // Assert that the DOM element hasn't been replaced.
167 + // Note that we cannot use expect(serverNode).toBe(clientNode) because
168 + // of jest bug #1772.
169 + expect(serverNode === clientNode).toBe(true);
170 serverNode = serverNode.nextSibling;
171 clientNode = clientNode.nextSibling;
172 }
@@ -180,7 +180,7 @@ const clientRenderOnBadMarkup = async (element, errorCount = 0) => {
180 var domElement = document.createElement('div');
181 domElement.innerHTML =
182 '<div id="badIdWhichWillCauseMismatch" data-reactroot="" data-reactid="1"></div>';
183 - await renderIntoDom(element, domElement, errorCount + 1);
183 + await renderIntoDom(element, domElement, true, errorCount + 1);
184
185 // This gives us the resulting text content.
186 var hydratedTextContent = domElement.textContent;
@@ -188,7 +188,7 @@ const clientRenderOnBadMarkup = async (element, errorCount = 0) => {
188 // Next we render the element into a clean DOM node client side.
189 const cleanDomElement = document.createElement('div');
190 ExecutionEnvironment.canUseDOM = true;
191 - await asyncReactDOMRender(element, cleanDomElement);
191 + await asyncReactDOMRender(element, cleanDomElement, true);
192 ExecutionEnvironment.canUseDOM = false;
193 // This gives us the expected text content.
194 const cleanTextContent = cleanDomElement.textContent;
@@ -296,6 +296,7 @@ async function testMarkupMatch(serverElement, clientElement, shouldMatch) {
296 return renderIntoDom(
297 clientElement,
298 domElement.parentNode,
299 + true,
300 shouldMatch ? 0 : 1,
301 );
302 }
@@ -1971,7 +1972,11 @@ describe('ReactDOMServerIntegration', () => {
1972
1973 resetModules();
1974 // client render on top of the server markup.
1974 - const clientField = await renderIntoDom(element, field.parentNode);
1975 + const clientField = await renderIntoDom(
1976 + element,
1977 + field.parentNode,
1978 + true,
1979 + );
1980 // verify that the input field was not replaced.
1981 // Note that we cannot use expect(clientField).toBe(field) because
1982 // of jest bug #1772
@@ -2330,6 +2335,7 @@ describe('ReactDOMServerIntegration', () => {
2335 await asyncReactDOMRender(
2336 <RefsComponent ref={e => (component = e)} />,
2337 root,
2338 + true,
2339 );
2340 expect(component.refs.myDiv).toBe(root.firstChild);
2341 });
src/renderers/dom/shared/__tests__/ReactDOMTextComponent-test.js
+12 -2
@@ -14,6 +14,7 @@
14 var React;
15 var ReactDOM;
16 var ReactDOMServer;
17 +var ReactDOMFeatureFlags;
18
19 // In standard React, TextComponent keeps track of different Text templates
20 // using comments. However, in React Fiber, those comments are not outputted due
@@ -29,6 +30,7 @@ describe('ReactDOMTextComponent', () => {
30 React = require('react');
31 ReactDOM = require('react-dom');
32 ReactDOMServer = require('react-dom/server');
33 + ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
34 });
35
36 it('updates a mounted text component in place', () => {
@@ -117,7 +119,11 @@ describe('ReactDOMTextComponent', () => {
119 var reactEl = <div>{'foo'}{'bar'}{'baz'}</div>;
120 el.innerHTML = ReactDOMServer.renderToString(reactEl);
121
120 - ReactDOM.render(reactEl, el);
122 + if (ReactDOMFeatureFlags.useFiber) {
123 + ReactDOM.hydrate(reactEl, el);
124 + } else {
125 + ReactDOM.render(reactEl, el);
126 + }
127 expect(el.textContent).toBe('foobarbaz');
128
129 ReactDOM.unmountComponentAtNode(el);
@@ -125,7 +131,11 @@ describe('ReactDOMTextComponent', () => {
131 reactEl = <div>{''}{''}{''}</div>;
132 el.innerHTML = ReactDOMServer.renderToString(reactEl);
133
128 - ReactDOM.render(reactEl, el);
134 + if (ReactDOMFeatureFlags.useFiber) {
135 + ReactDOM.hydrate(reactEl, el);
136 + } else {
137 + ReactDOM.render(reactEl, el);
138 + }
139 expect(el.textContent).toBe('');
140 });
141
src/renderers/dom/shared/__tests__/ReactMount-test.js
+36 -12
@@ -146,12 +146,16 @@ describe('ReactMount', () => {
146 expect(instance1 === instance2).toBe(true);
147 });
148
149 - it('should warn if mounting into dirty rendered markup', () => {
149 + it('should warn if mounting into left padded rendered markup', () => {
150 var container = document.createElement('container');
151 container.innerHTML = ReactDOMServer.renderToString(<div />) + ' ';
152
153 spyOn(console, 'error');
154 - ReactDOM.render(<div />, container);
154 + if (ReactDOMFeatureFlags.useFiber) {
155 + ReactDOM.hydrate(<div />, container);
156 + } else {
157 + ReactDOM.render(<div />, container);
158 + }
159 expectDev(console.error.calls.count()).toBe(1);
160 if (ReactDOMFeatureFlags.useFiber) {
161 expectDev(console.error.calls.argsFor(0)[0]).toContain(
@@ -162,15 +166,28 @@ describe('ReactMount', () => {
166 'Target node has markup rendered by React, but there are unrelated nodes as well.',
167 );
168 }
169 + });
170
166 - console.error.calls.reset();
167 - ReactDOM.unmountComponentAtNode(container);
171 + it('should warn if mounting into right padded rendered markup', () => {
172 + var container = document.createElement('container');
173 container.innerHTML = ' ' + ReactDOMServer.renderToString(<div />);
169 - ReactDOM.render(<div />, container);
174 +
175 + spyOn(console, 'error');
176 + if (ReactDOMFeatureFlags.useFiber) {
177 + ReactDOM.hydrate(<div />, container);
178 + } else {
179 + ReactDOM.render(<div />, container);
180 + }
181 expectDev(console.error.calls.count()).toBe(1);
171 - expectDev(console.error.calls.argsFor(0)[0]).toContain(
172 - 'Target node has markup rendered by React, but there are unrelated nodes as well.',
173 - );
182 + if (ReactDOMFeatureFlags.useFiber) {
183 + expectDev(console.error.calls.argsFor(0)[0]).toContain(
184 + 'Did not expect server HTML to contain the text node " " in <container>.',
185 + );
186 + } else {
187 + expectDev(console.error.calls.argsFor(0)[0]).toContain(
188 + 'Target node has markup rendered by React, but there are unrelated nodes as well.',
189 + );
190 + }
191 });
192
193 it('should not warn if mounting into non-empty node', () => {
@@ -203,10 +220,17 @@ describe('ReactMount', () => {
220 div.innerHTML = markup;
221
222 spyOn(console, 'error');
206 - ReactDOM.render(
207 - <div>This markup contains an nbsp entity: &nbsp; client text</div>,
208 - div,
209 - );
223 + if (ReactDOMFeatureFlags.useFiber) {
224 + ReactDOM.hydrate(
225 + <div>This markup contains an nbsp entity: &nbsp; client text</div>,
226 + div,
227 + );
228 + } else {
229 + ReactDOM.render(
230 + <div>This markup contains an nbsp entity: &nbsp; client text</div>,
231 + div,
232 + );
233 + }
234 expectDev(console.error.calls.count()).toBe(1);
235 if (ReactDOMFeatureFlags.useFiber) {
236 expectDev(console.error.calls.argsFor(0)[0]).toContain(
src/renderers/dom/shared/__tests__/ReactRenderDocument-test.js
+474 -205
@@ -14,10 +14,11 @@
14 var React;
15 var ReactDOM;
16 var ReactDOMServer;
17 -var ReactDOMFeatureFlags;
17
18 var getTestDocument;
19
20 +var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
21 +
22 var UNMOUNT_INVARIANT_MESSAGE =
23 '<html> tried to unmount. ' +
24 'Because of cross-browser quirks it is impossible to unmount some ' +
@@ -32,256 +33,524 @@ describe('rendering React components at document', () => {
33 React = require('react');
34 ReactDOM = require('react-dom');
35 ReactDOMServer = require('react-dom/server');
35 - ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
36 getTestDocument = require('getTestDocument');
37 });
38
39 - it('should be able to adopt server markup', () => {
40 - class Root extends React.Component {
41 - render() {
42 - return (
43 - <html>
44 - <head>
45 - <title>Hello World</title>
46 - </head>
47 - <body>
48 - {'Hello ' + this.props.hello}
49 - </body>
50 - </html>
39 + describe('with old implicit hydration API', () => {
40 + function expectDeprecationWarningWithFiber() {
41 + if (ReactDOMFeatureFlags.useFiber) {
42 + expectDev(console.warn.calls.count()).toBe(1);
43 + expectDev(console.warn.calls.argsFor(0)[0]).toContain(
44 + 'render(): Calling ReactDOM.render() to hydrate server-rendered markup ' +
45 + 'will stop working in React v17. Replace the ReactDOM.render() call ' +
46 + 'with ReactDOM.hydrate() if you want React to attach to the server HTML.',
47 );
48 + } else {
49 + expectDev(console.warn.calls.count()).toBe(0);
50 }
51 }
52
55 - var markup = ReactDOMServer.renderToString(<Root hello="world" />);
56 - var testDocument = getTestDocument(markup);
57 - var body = testDocument.body;
58 -
59 - ReactDOM.render(<Root hello="world" />, testDocument);
60 - expect(testDocument.body.innerHTML).toBe('Hello world');
53 + it('should be able to adopt server markup', () => {
54 + spyOn(console, 'warn');
55 + class Root extends React.Component {
56 + render() {
57 + return (
58 + <html>
59 + <head>
60 + <title>Hello World</title>
61 + </head>
62 + <body>
63 + {'Hello ' + this.props.hello}
64 + </body>
65 + </html>
66 + );
67 + }
68 + }
69
62 - ReactDOM.render(<Root hello="moon" />, testDocument);
63 - expect(testDocument.body.innerHTML).toBe('Hello moon');
70 + var markup = ReactDOMServer.renderToString(<Root hello="world" />);
71 + var testDocument = getTestDocument(markup);
72 + var body = testDocument.body;
73
65 - expect(body === testDocument.body).toBe(true);
66 - });
74 + ReactDOM.render(<Root hello="world" />, testDocument);
75 + expect(testDocument.body.innerHTML).toBe('Hello world');
76
68 - it('should not be able to unmount component from document node', () => {
69 - class Root extends React.Component {
70 - render() {
71 - return (
72 - <html>
73 - <head>
74 - <title>Hello World</title>
75 - </head>
76 - <body>
77 - Hello world
78 - </body>
79 - </html>
80 - );
77 + ReactDOM.render(<Root hello="moon" />, testDocument);
78 + expect(testDocument.body.innerHTML).toBe('Hello moon');
79 +
80 + expect(body === testDocument.body).toBe(true);
81 + expectDeprecationWarningWithFiber();
82 + });
83 +
84 + it('should not be able to unmount component from document node', () => {
85 + spyOn(console, 'warn');
86 + class Root extends React.Component {
87 + render() {
88 + return (
89 + <html>
90 + <head>
91 + <title>Hello World</title>
92 + </head>
93 + <body>
94 + Hello world
95 + </body>
96 + </html>
97 + );
98 + }
99 }
82 - }
83 -
84 - var markup = ReactDOMServer.renderToString(<Root />);
85 - var testDocument = getTestDocument(markup);
86 - ReactDOM.render(<Root />, testDocument);
87 - expect(testDocument.body.innerHTML).toBe('Hello world');
88 -
89 - if (ReactDOMFeatureFlags.useFiber) {
90 - // In Fiber this actually works. It might not be a good idea though.
91 - ReactDOM.unmountComponentAtNode(testDocument);
92 - expect(testDocument.firstChild).toBe(null);
93 - } else {
94 - expect(function() {
95 - ReactDOM.unmountComponentAtNode(testDocument);
96 - }).toThrowError(UNMOUNT_INVARIANT_MESSAGE);
100
101 + var markup = ReactDOMServer.renderToString(<Root />);
102 + var testDocument = getTestDocument(markup);
103 + ReactDOM.render(<Root />, testDocument);
104 expect(testDocument.body.innerHTML).toBe('Hello world');
99 - }
100 - });
105
102 - it('should not be able to switch root constructors', () => {
103 - class Component extends React.Component {
104 - render() {
105 - return (
106 - <html>
107 - <head>
108 - <title>Hello World</title>
109 - </head>
110 - <body>
111 - Hello world
112 - </body>
113 - </html>
114 - );
106 + if (ReactDOMFeatureFlags.useFiber) {
107 + // In Fiber this actually works. It might not be a good idea though.
108 + ReactDOM.unmountComponentAtNode(testDocument);
109 + expect(testDocument.firstChild).toBe(null);
110 + } else {
111 + expect(function() {
112 + ReactDOM.unmountComponentAtNode(testDocument);
113 + }).toThrowError(UNMOUNT_INVARIANT_MESSAGE);
114 +
115 + expect(testDocument.body.innerHTML).toBe('Hello world');
116 }
116 - }
117
118 - class Component2 extends React.Component {
119 - render() {
120 - return (
121 - <html>
122 - <head>
123 - <title>Hello World</title>
124 - </head>
125 - <body>
126 - Goodbye world
127 - </body>
128 - </html>
129 - );
118 + expectDeprecationWarningWithFiber();
119 + });
120 +
121 + it('should not be able to switch root constructors', () => {
122 + spyOn(console, 'warn');
123 + class Component extends React.Component {
124 + render() {
125 + return (
126 + <html>
127 + <head>
128 + <title>Hello World</title>
129 + </head>
130 + <body>
131 + Hello world
132 + </body>
133 + </html>
134 + );
135 + }
136 }
131 - }
137
133 - var markup = ReactDOMServer.renderToString(<Component />);
134 - var testDocument = getTestDocument(markup);
138 + class Component2 extends React.Component {
139 + render() {
140 + return (
141 + <html>
142 + <head>
143 + <title>Hello World</title>
144 + </head>
145 + <body>
146 + Goodbye world
147 + </body>
148 + </html>
149 + );
150 + }
151 + }
152
136 - ReactDOM.render(<Component />, testDocument);
153 + var markup = ReactDOMServer.renderToString(<Component />);
154 + var testDocument = getTestDocument(markup);
155
138 - expect(testDocument.body.innerHTML).toBe('Hello world');
156 + ReactDOM.render(<Component />, testDocument);
157
140 - // Reactive update
141 - if (ReactDOMFeatureFlags.useFiber) {
142 - // This works but is probably a bad idea.
143 - ReactDOM.render(<Component2 />, testDocument);
158 + expect(testDocument.body.innerHTML).toBe('Hello world');
159
145 - expect(testDocument.body.innerHTML).toBe('Goodbye world');
146 - } else {
147 - expect(function() {
160 + // Reactive update
161 + if (ReactDOMFeatureFlags.useFiber) {
162 + // This works but is probably a bad idea.
163 ReactDOM.render(<Component2 />, testDocument);
149 - }).toThrowError(UNMOUNT_INVARIANT_MESSAGE);
164
151 - expect(testDocument.body.innerHTML).toBe('Hello world');
152 - }
153 - });
165 + expect(testDocument.body.innerHTML).toBe('Goodbye world');
166 + } else {
167 + expect(function() {
168 + ReactDOM.render(<Component2 />, testDocument);
169 + }).toThrowError(UNMOUNT_INVARIANT_MESSAGE);
170
155 - it('should be able to mount into document', () => {
156 - class Component extends React.Component {
157 - render() {
158 - return (
159 - <html>
160 - <head>
161 - <title>Hello World</title>
162 - </head>
163 - <body>
164 - {this.props.text}
165 - </body>
166 - </html>
167 - );
171 + expect(testDocument.body.innerHTML).toBe('Hello world');
172 }
169 - }
173
171 - var markup = ReactDOMServer.renderToString(
172 - <Component text="Hello world" />,
173 - );
174 - var testDocument = getTestDocument(markup);
174 + expectDeprecationWarningWithFiber();
175 + });
176 +
177 + it('should be able to mount into document', () => {
178 + spyOn(console, 'warn');
179 + class Component extends React.Component {
180 + render() {
181 + return (
182 + <html>
183 + <head>
184 + <title>Hello World</title>
185 + </head>
186 + <body>
187 + {this.props.text}
188 + </body>
189 + </html>
190 + );
191 + }
192 + }
193
176 - ReactDOM.render(<Component text="Hello world" />, testDocument);
194 + var markup = ReactDOMServer.renderToString(
195 + <Component text="Hello world" />,
196 + );
197 + var testDocument = getTestDocument(markup);
198
178 - expect(testDocument.body.innerHTML).toBe('Hello world');
179 - });
199 + ReactDOM.render(<Component text="Hello world" />, testDocument);
200
181 - it('renders over an existing text child without throwing', () => {
182 - const container = document.createElement('div');
183 - container.textContent = 'potato';
184 - ReactDOM.render(<div>parsnip</div>, container);
185 - expect(container.textContent).toBe('parsnip');
186 - });
201 + expect(testDocument.body.innerHTML).toBe('Hello world');
202 + expectDeprecationWarningWithFiber();
203 + });
204 +
205 + it('renders over an existing text child without throwing', () => {
206 + const container = document.createElement('div');
207 + container.textContent = 'potato';
208 + ReactDOM.render(<div>parsnip</div>, container);
209 + expect(container.textContent).toBe('parsnip');
210 + // We don't expect a warning about new hydration API here because
211 + // we aren't sure if the user meant to hydrate or replace a stub node.
212 + // We would see a warning if the container had React-rendered HTML in it.
213 + });
214 +
215 + it('should give helpful errors on state desync', () => {
216 + class Component extends React.Component {
217 + render() {
218 + return (
219 + <html>
220 + <head>
221 + <title>Hello World</title>
222 + </head>
223 + <body>
224 + {this.props.text}
225 + </body>
226 + </html>
227 + );
228 + }
229 + }
230
188 - it('should give helpful errors on state desync', () => {
189 - class Component extends React.Component {
190 - render() {
191 - return (
192 - <html>
193 - <head>
194 - <title>Hello World</title>
195 - </head>
196 - <body>
197 - {this.props.text}
198 - </body>
199 - </html>
231 + var markup = ReactDOMServer.renderToString(
232 + <Component text="Goodbye world" />,
233 + );
234 + var testDocument = getTestDocument(markup);
235 +
236 + if (ReactDOMFeatureFlags.useFiber) {
237 + spyOn(console, 'warn');
238 + spyOn(console, 'error');
239 + ReactDOM.render(<Component text="Hello world" />, testDocument);
240 + expect(testDocument.body.innerHTML).toBe('Hello world');
241 + expectDev(console.warn.calls.count()).toBe(1);
242 + expectDev(console.warn.calls.argsFor(0)[0]).toContain(
243 + 'render(): Calling ReactDOM.render() to hydrate server-rendered markup ' +
244 + 'will stop working in React v17. Replace the ReactDOM.render() call ' +
245 + 'with ReactDOM.hydrate() if you want React to attach to the server HTML.',
246 + );
247 + expectDev(console.error.calls.count()).toBe(1);
248 + expectDev(console.error.calls.argsFor(0)[0]).toContain(
249 + 'Warning: Text content did not match.',
250 + );
251 + } else {
252 + expect(function() {
253 + // Notice the text is different!
254 + ReactDOM.render(<Component text="Hello world" />, testDocument);
255 + }).toThrowError(
256 + "You're trying to render a component to the document using " +
257 + 'server rendering but the checksum was invalid. This usually ' +
258 + 'means you rendered a different component type or props on ' +
259 + 'the client from the one on the server, or your render() methods ' +
260 + 'are impure. React cannot handle this case due to cross-browser ' +
261 + 'quirks by rendering at the document root. You should look for ' +
262 + 'environment dependent code in your components and ensure ' +
263 + 'the props are the same client and server side:\n' +
264 + ' (client) dy data-reactid="4">Hello world</body></\n' +
265 + ' (server) dy data-reactid="4">Goodbye world</body>',
266 );
267 }
202 - }
203 -
204 - var markup = ReactDOMServer.renderToString(
205 - <Component text="Goodbye world" />,
206 - );
207 - var testDocument = getTestDocument(markup);
268 + });
269 +
270 + it('should throw on full document render w/ no markup', () => {
271 + var testDocument = getTestDocument();
272 +
273 + class Component extends React.Component {
274 + render() {
275 + return (
276 + <html>
277 + <head>
278 + <title>Hello World</title>
279 + </head>
280 + <body>
281 + {this.props.text}
282 + </body>
283 + </html>
284 + );
285 + }
286 + }
287
209 - if (ReactDOMFeatureFlags.useFiber) {
210 - spyOn(console, 'error');
211 - ReactDOM.render(<Component text="Hello world" />, testDocument);
212 - expect(testDocument.body.innerHTML).toBe('Hello world');
213 - expectDev(console.error.calls.count()).toBe(1);
214 - expectDev(console.error.calls.argsFor(0)[0]).toContain(
215 - 'Warning: Text content did not match.',
216 - );
217 - } else {
218 - expect(function() {
219 - // Notice the text is different!
288 + if (ReactDOMFeatureFlags.useFiber) {
289 ReactDOM.render(<Component text="Hello world" />, testDocument);
221 - }).toThrowError(
222 - "You're trying to render a component to the document using " +
223 - 'server rendering but the checksum was invalid. This usually ' +
224 - 'means you rendered a different component type or props on ' +
225 - 'the client from the one on the server, or your render() methods ' +
226 - 'are impure. React cannot handle this case due to cross-browser ' +
227 - 'quirks by rendering at the document root. You should look for ' +
228 - 'environment dependent code in your components and ensure ' +
229 - 'the props are the same client and server side:\n' +
230 - ' (client) dy data-reactid="4">Hello world</body></\n' +
231 - ' (server) dy data-reactid="4">Goodbye world</body>',
290 + expect(testDocument.body.innerHTML).toBe('Hello world');
291 + } else {
292 + expect(function() {
293 + ReactDOM.render(<Component />, testDocument);
294 + }).toThrowError(
295 + "You're trying to render a component to the document but you didn't " +
296 + "use server rendering. We can't do this without using server " +
297 + 'rendering due to cross-browser quirks. See ' +
298 + 'ReactDOMServer.renderToString() for server rendering.',
299 + );
300 + }
301 + // We don't expect a warning about new hydration API here because
302 + // we aren't sure if the user meant to hydrate or replace the document.
303 + // We would see a warning if the document had React-rendered HTML in it.
304 + });
305 +
306 + it('supports findDOMNode on full-page components', () => {
307 + spyOn(console, 'warn');
308 + var tree = (
309 + <html>
310 + <head>
311 + <title>Hello World</title>
312 + </head>
313 + <body>
314 + Hello world
315 + </body>
316 + </html>
317 );
233 - }
318 +
319 + var markup = ReactDOMServer.renderToString(tree);
320 + var testDocument = getTestDocument(markup);
321 + var component = ReactDOM.render(tree, testDocument);
322 + expect(testDocument.body.innerHTML).toBe('Hello world');
323 + expect(ReactDOM.findDOMNode(component).tagName).toBe('HTML');
324 + expectDeprecationWarningWithFiber();
325 + });
326 });
327
236 - it('should throw on full document render w/ no markup', () => {
237 - var testDocument = getTestDocument();
328 + if (ReactDOMFeatureFlags.useFiber) {
329 + describe('with new explicit hydration API', () => {
330 + it('warns if there is no server rendered markup to hydrate', () => {
331 + spyOn(console, 'error');
332 + const container = document.createElement('div');
333 + ReactDOM.hydrate(<div />, container);
334 + expectDev(console.error.calls.count()).toBe(1);
335 + expectDev(console.error.calls.argsFor(0)[0]).toContain(
336 + 'hydrate(): Expected to hydrate from server-rendered markup, but the passed ' +
337 + 'DOM container node was empty. React will create the DOM from scratch.',
338 + );
339 + });
340 +
341 + it('should be able to adopt server markup', () => {
342 + class Root extends React.Component {
343 + render() {
344 + return (
345 + <html>
346 + <head>
347 + <title>Hello World</title>
348 + </head>
349 + <body>
350 + {'Hello ' + this.props.hello}
351 + </body>
352 + </html>
353 + );
354 + }
355 + }
356 +
357 + var markup = ReactDOMServer.renderToString(<Root hello="world" />);
358 + var testDocument = getTestDocument(markup);
359 + var body = testDocument.body;
360 +
361 + ReactDOM.hydrate(<Root hello="world" />, testDocument);
362 + expect(testDocument.body.innerHTML).toBe('Hello world');
363 +
364 + ReactDOM.hydrate(<Root hello="moon" />, testDocument);
365 + expect(testDocument.body.innerHTML).toBe('Hello moon');
366 +
367 + expect(body === testDocument.body).toBe(true);
368 + });
369 +
370 + it('should not be able to unmount component from document node', () => {
371 + class Root extends React.Component {
372 + render() {
373 + return (
374 + <html>
375 + <head>
376 + <title>Hello World</title>
377 + </head>
378 + <body>
379 + Hello world
380 + </body>
381 + </html>
382 + );
383 + }
384 + }
385 +
386 + var markup = ReactDOMServer.renderToString(<Root />);
387 + var testDocument = getTestDocument(markup);
388 + ReactDOM.hydrate(<Root />, testDocument);
389 + expect(testDocument.body.innerHTML).toBe('Hello world');
390 +
391 + // In Fiber this actually works. It might not be a good idea though.
392 + ReactDOM.unmountComponentAtNode(testDocument);
393 + expect(testDocument.firstChild).toBe(null);
394 + });
395 +
396 + it('should not be able to switch root constructors', () => {
397 + class Component extends React.Component {
398 + render() {
399 + return (
400 + <html>
401 + <head>
402 + <title>Hello World</title>
403 + </head>
404 + <body>
405 + Hello world
406 + </body>
407 + </html>
408 + );
409 + }
410 + }
411 +
412 + class Component2 extends React.Component {
413 + render() {
414 + return (
415 + <html>
416 + <head>
417 + <title>Hello World</title>
418 + </head>
419 + <body>
420 + Goodbye world
421 + </body>
422 + </html>
423 + );
424 + }
425 + }
426 +
427 + var markup = ReactDOMServer.renderToString(<Component />);
428 + var testDocument = getTestDocument(markup);
429 +
430 + ReactDOM.hydrate(<Component />, testDocument);
431 +
432 + expect(testDocument.body.innerHTML).toBe('Hello world');
433 +
434 + // This works but is probably a bad idea.
435 + ReactDOM.hydrate(<Component2 />, testDocument);
436 +
437 + expect(testDocument.body.innerHTML).toBe('Goodbye world');
438 + });
439 +
440 + it('should be able to mount into document', () => {
441 + class Component extends React.Component {
442 + render() {
443 + return (
444 + <html>
445 + <head>
446 + <title>Hello World</title>
447 + </head>
448 + <body>
449 + {this.props.text}
450 + </body>
451 + </html>
452 + );
453 + }
454 + }
455 +
456 + var markup = ReactDOMServer.renderToString(
457 + <Component text="Hello world" />,
458 + );
459 + var testDocument = getTestDocument(markup);
460 +
461 + ReactDOM.hydrate(<Component text="Hello world" />, testDocument);
462 +
463 + expect(testDocument.body.innerHTML).toBe('Hello world');
464 + });
465 +
466 + it('renders over an existing text child without throwing', () => {
467 + spyOn(console, 'error');
468 + const container = document.createElement('div');
469 + container.textContent = 'potato';
470 + ReactDOM.hydrate(<div>parsnip</div>, container);
471 + expect(container.textContent).toBe('parsnip');
472 + expectDev(console.error.calls.count()).toBe(1);
473 + expectDev(console.error.calls.argsFor(0)[0]).toContain(
474 + 'Did not expect server HTML to contain the text node "potato" in <div>.',
475 + );
476 + });
477 +
478 + it('should give helpful errors on state desync', () => {
479 + class Component extends React.Component {
480 + render() {
481 + return (
482 + <html>
483 + <head>
484 + <title>Hello World</title>
485 + </head>
486 + <body>
487 + {this.props.text}
488 + </body>
489 + </html>
490 + );
491 + }
492 + }
493 +
494 + var markup = ReactDOMServer.renderToString(
495 + <Component text="Goodbye world" />,
496 + );
497 + var testDocument = getTestDocument(markup);
498 +
499 + spyOn(console, 'error');
500 + ReactDOM.hydrate(<Component text="Hello world" />, testDocument);
501 + expect(testDocument.body.innerHTML).toBe('Hello world');
502 + expectDev(console.error.calls.count()).toBe(1);
503 + expectDev(console.error.calls.argsFor(0)[0]).toContain(
504 + 'Warning: Text content did not match.',
505 + );
506 + });
507 +
508 + it('should render w/ no markup to full document', () => {
509 + spyOn(console, 'error');
510 + var testDocument = getTestDocument();
511 +
512 + class Component extends React.Component {
513 + render() {
514 + return (
515 + <html>
516 + <head>
517 + <title>Hello World</title>
518 + </head>
519 + <body>
520 + {this.props.text}
521 + </body>
522 + </html>
523 + );
524 + }
525 + }
526 +
527 + ReactDOM.hydrate(<Component text="Hello world" />, testDocument);
528 + expect(testDocument.body.innerHTML).toBe('Hello world');
529 + expectDev(console.error.calls.count()).toBe(1);
530 + expectDev(console.error.calls.argsFor(0)[0]).toContain(
531 + // getTestDocument() has an extra <meta> that we didn't render.
532 + 'Did not expect server HTML to contain a <meta> in <head>.',
533 + );
534 + });
535
239 - class Component extends React.Component {
240 - render() {
241 - return (
536 + it('supports findDOMNode on full-page components', () => {
537 + var tree = (
538 <html>
539 <head>
540 <title>Hello World</title>
541 </head>
542 <body>
247 - {this.props.text}
543 + Hello world
544 </body>
545 </html>
546 );
251 - }
252 - }
547
254 - if (ReactDOMFeatureFlags.useFiber) {
255 - ReactDOM.render(<Component text="Hello world" />, testDocument);
256 - expect(testDocument.body.innerHTML).toBe('Hello world');
257 - } else {
258 - expect(function() {
259 - ReactDOM.render(<Component />, testDocument);
260 - }).toThrowError(
261 - "You're trying to render a component to the document but you didn't " +
262 - "use server rendering. We can't do this without using server " +
263 - 'rendering due to cross-browser quirks. See ' +
264 - 'ReactDOMServer.renderToString() for server rendering.',
265 - );
266 - }
267 - });
268 -
269 - it('supports findDOMNode on full-page components', () => {
270 - var tree = (
271 - <html>
272 - <head>
273 - <title>Hello World</title>
274 - </head>
275 - <body>
276 - Hello world
277 - </body>
278 - </html>
279 - );
280 -
281 - var markup = ReactDOMServer.renderToString(tree);
282 - var testDocument = getTestDocument(markup);
283 - var component = ReactDOM.render(tree, testDocument);
284 - expect(testDocument.body.innerHTML).toBe('Hello world');
285 - expect(ReactDOM.findDOMNode(component).tagName).toBe('HTML');
286 - });
548 + var markup = ReactDOMServer.renderToString(tree);
549 + var testDocument = getTestDocument(markup);
550 + var component = ReactDOM.hydrate(tree, testDocument);
551 + expect(testDocument.body.innerHTML).toBe('Hello world');
552 + expect(ReactDOM.findDOMNode(component).tagName).toBe('HTML');
553 + });
554 + });
555 + }
556 });
src/renderers/dom/shared/__tests__/ReactServerRendering-test.js
+135 -22
@@ -14,7 +14,6 @@
14 var ExecutionEnvironment;
15 var React;
16 var ReactDOM;
17 -var ReactDOMFeatureFlags;
17 var ReactDOMServer;
18 var ReactMarkupChecksum;
19 var ReactReconcileTransaction;
@@ -22,6 +21,8 @@ var ReactTestUtils;
21 var PropTypes;
22 var ReactFeatureFlags;
23
24 +var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
25 +
26 var ID_ATTRIBUTE_NAME;
27 var ROOT_ATTRIBUTE_NAME;
28
@@ -31,7 +32,6 @@ describe('ReactDOMServer', () => {
32 React = require('react');
33 ReactDOM = require('react-dom');
34 ReactTestUtils = require('react-dom/test-utils');
34 - ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
35 ReactMarkupChecksum = require('ReactMarkupChecksum');
36 ReactReconcileTransaction = require('ReactReconcileTransaction');
37 PropTypes = require('prop-types');
@@ -55,9 +55,10 @@ describe('ReactDOMServer', () => {
55 new RegExp(
56 '<span ' +
57 ROOT_ATTRIBUTE_NAME +
58 - '="" ' +
59 - ID_ATTRIBUTE_NAME +
60 - '="[^"]*"' +
58 + '=""' +
59 + (ReactDOMFeatureFlags.useFiber
60 + ? ''
61 + : ' ' + ID_ATTRIBUTE_NAME + '="[^"]*"') +
62 (ReactDOMFeatureFlags.useFiber
63 ? ''
64 : ' ' + ReactMarkupChecksum.CHECKSUM_ATTR_NAME + '="[^"]+"') +
@@ -72,9 +73,10 @@ describe('ReactDOMServer', () => {
73 new RegExp(
74 '<img ' +
75 ROOT_ATTRIBUTE_NAME +
75 - '="" ' +
76 - ID_ATTRIBUTE_NAME +
77 - '="[^"]*"' +
76 + '=""' +
77 + (ReactDOMFeatureFlags.useFiber
78 + ? ''
79 + : ' ' + ID_ATTRIBUTE_NAME + '="[^"]*"') +
80 (ReactDOMFeatureFlags.useFiber
81 ? ''
82 : ' ' + ReactMarkupChecksum.CHECKSUM_ATTR_NAME + '="[^"]+"') +
@@ -89,9 +91,10 @@ describe('ReactDOMServer', () => {
91 new RegExp(
92 '<img data-attr="&gt;" ' +
93 ROOT_ATTRIBUTE_NAME +
92 - '="" ' +
93 - ID_ATTRIBUTE_NAME +
94 - '="[^"]*"' +
94 + '=""' +
95 + (ReactDOMFeatureFlags.useFiber
96 + ? ''
97 + : ' ' + ID_ATTRIBUTE_NAME + '="[^"]*"') +
98 (ReactDOMFeatureFlags.useFiber
99 ? ''
100 : ' ' + ReactMarkupChecksum.CHECKSUM_ATTR_NAME + '="[^"]+"') +
@@ -135,16 +138,19 @@ describe('ReactDOMServer', () => {
138 new RegExp(
139 '<div ' +
140 ROOT_ATTRIBUTE_NAME +
138 - '="" ' +
139 - ID_ATTRIBUTE_NAME +
140 - '="[^"]*"' +
141 + '=""' +
142 + (ReactDOMFeatureFlags.useFiber
143 + ? ''
144 + : ' ' + ID_ATTRIBUTE_NAME + '="[^"]*"') +
145 (ReactDOMFeatureFlags.useFiber
146 ? ''
147 : ' ' + ReactMarkupChecksum.CHECKSUM_ATTR_NAME + '="[^"]+"') +
148 '>' +
145 - '<span ' +
146 - ID_ATTRIBUTE_NAME +
147 - '="[^"]*">' +
149 + '<span' +
150 + (ReactDOMFeatureFlags.useFiber
151 + ? ''
152 + : ' ' + ID_ATTRIBUTE_NAME + '="[^"]*"') +
153 + '>' +
154 (ReactDOMFeatureFlags.useFiber
155 ? 'My name is <!-- -->child'
156 : '<!-- react-text: [0-9]+ -->My name is <!-- /react-text -->' +
@@ -206,9 +212,10 @@ describe('ReactDOMServer', () => {
212 new RegExp(
213 '<span ' +
214 ROOT_ATTRIBUTE_NAME +
209 - '="" ' +
210 - ID_ATTRIBUTE_NAME +
211 - '="[^"]*"' +
215 + '=""' +
216 + (ReactDOMFeatureFlags.useFiber
217 + ? ''
218 + : ' ' + ID_ATTRIBUTE_NAME + '="[^"]*"') +
219 (ReactDOMFeatureFlags.useFiber
220 ? ''
221 : ' ' + ReactMarkupChecksum.CHECKSUM_ATTR_NAME + '="[^"]+"') +
@@ -234,7 +241,9 @@ describe('ReactDOMServer', () => {
241 runTest();
242 });
243
237 - it('should have the correct mounting behavior', () => {
244 + it('should have the correct mounting behavior (old hydrate API)', () => {
245 + spyOn(console, 'warn');
246 + spyOn(console, 'error');
247 // This test is testing client-side behavior.
248 ExecutionEnvironment.canUseDOM = true;
249
@@ -289,6 +298,17 @@ describe('ReactDOMServer', () => {
298
299 var instance = ReactDOM.render(<TestComponent name="x" />, element);
300 expect(mountCount).toEqual(3);
301 + if (ReactDOMFeatureFlags.useFiber) {
302 + expectDev(console.warn.calls.count()).toBe(1);
303 + expectDev(console.warn.calls.argsFor(0)[0]).toContain(
304 + 'render(): Calling ReactDOM.render() to hydrate server-rendered markup ' +
305 + 'will stop working in React v17. Replace the ReactDOM.render() call ' +
306 + 'with ReactDOM.hydrate() if you want React to attach to the server HTML.',
307 + );
308 + } else {
309 + expectDev(console.warn.calls.count()).toBe(0);
310 + }
311 + console.warn.calls.reset();
312
313 var expectedMarkup = lastMarkup;
314 if (ReactDOMFeatureFlags.useFiber) {
@@ -308,7 +328,6 @@ describe('ReactDOMServer', () => {
328 // Now simulate a situation where the app is not idempotent. React should
329 // warn but do the right thing.
330 element.innerHTML = lastMarkup;
311 - spyOn(console, 'error');
331 instance = ReactDOM.render(<TestComponent name="y" />, element);
332 expect(mountCount).toEqual(4);
333 expectDev(console.error.calls.count()).toBe(1);
@@ -324,6 +343,7 @@ describe('ReactDOMServer', () => {
343 '(server) -- react-text: 3 -->x<!-- /react-text --',
344 );
345 }
346 + console.error.calls.reset();
347 expect(element.innerHTML.length > 0).toBe(true);
348 expect(element.innerHTML).not.toEqual(lastMarkup);
349
@@ -331,8 +351,101 @@ describe('ReactDOMServer', () => {
351 expect(numClicks).toEqual(1);
352 ReactTestUtils.Simulate.click(ReactDOM.findDOMNode(instance.refs.span));
353 expect(numClicks).toEqual(2);
354 + expectDev(console.warn.calls.count()).toBe(0);
355 + expectDev(console.error.calls.count()).toBe(0);
356 });
357
358 + if (ReactDOMFeatureFlags.useFiber) {
359 + it('should have the correct mounting behavior (new hydrate API)', () => {
360 + spyOn(console, 'error');
361 + // This test is testing client-side behavior.
362 + ExecutionEnvironment.canUseDOM = true;
363 +
364 + var mountCount = 0;
365 + var numClicks = 0;
366 +
367 + class TestComponent extends React.Component {
368 + componentDidMount() {
369 + mountCount++;
370 + }
371 +
372 + click = () => {
373 + numClicks++;
374 + };
375 +
376 + render() {
377 + return (
378 + <span ref="span" onClick={this.click}>
379 + Name: {this.props.name}
380 + </span>
381 + );
382 + }
383 + }
384 +
385 + var element = document.createElement('div');
386 + ReactDOM.render(<TestComponent />, element);
387 +
388 + var lastMarkup = element.innerHTML;
389 +
390 + // Exercise the update path. Markup should not change,
391 + // but some lifecycle methods should be run again.
392 + ReactDOM.render(<TestComponent name="x" />, element);
393 + expect(mountCount).toEqual(1);
394 +
395 + // Unmount and remount. We should get another mount event and
396 + // we should get different markup, as the IDs are unique each time.
397 + ReactDOM.unmountComponentAtNode(element);
398 + expect(element.innerHTML).toEqual('');
399 + ReactDOM.render(<TestComponent name="x" />, element);
400 + expect(mountCount).toEqual(2);
401 + expect(element.innerHTML).not.toEqual(lastMarkup);
402 +
403 + // Now kill the node and render it on top of server-rendered markup, as if
404 + // we used server rendering. We should mount again, but the markup should
405 + // be unchanged. We will append a sentinel at the end of innerHTML to be
406 + // sure that innerHTML was not changed.
407 + ReactDOM.unmountComponentAtNode(element);
408 + expect(element.innerHTML).toEqual('');
409 +
410 + ExecutionEnvironment.canUseDOM = false;
411 + lastMarkup = ReactDOMServer.renderToString(<TestComponent name="x" />);
412 + ExecutionEnvironment.canUseDOM = true;
413 + element.innerHTML = lastMarkup;
414 +
415 + var instance = ReactDOM.hydrate(<TestComponent name="x" />, element);
416 + expect(mountCount).toEqual(3);
417 +
418 + var expectedMarkup = lastMarkup;
419 + if (ReactDOMFeatureFlags.useFiber) {
420 + var reactComments = /<!-- \/?react-text(: \d+)? -->/g;
421 + expectedMarkup = expectedMarkup.replace(reactComments, '');
422 + }
423 + expect(element.innerHTML).toBe(expectedMarkup);
424 +
425 + // Ensure the events system works after mount into server markup
426 + expect(numClicks).toEqual(0);
427 + ReactTestUtils.Simulate.click(ReactDOM.findDOMNode(instance.refs.span));
428 + expect(numClicks).toEqual(1);
429 +
430 + ReactDOM.unmountComponentAtNode(element);
431 + expect(element.innerHTML).toEqual('');
432 +
433 + // Now simulate a situation where the app is not idempotent. React should
434 + // warn but do the right thing.
435 + element.innerHTML = lastMarkup;
436 + instance = ReactDOM.hydrate(<TestComponent name="y" />, element);
437 + expect(mountCount).toEqual(4);
438 + expectDev(console.error.calls.count()).toBe(1);
439 + expect(element.innerHTML.length > 0).toBe(true);
440 + expect(element.innerHTML).not.toEqual(lastMarkup);
441 +
442 + // Ensure the events system works after markup mismatch.
443 + expect(numClicks).toEqual(1);
444 + ReactTestUtils.Simulate.click(ReactDOM.findDOMNode(instance.refs.span));
445 + expect(numClicks).toEqual(2);
446 + });
447 + }
448 +
449 it('should throw with silly args', () => {
450 expect(
451 ReactDOMServer.renderToString.bind(ReactDOMServer, {x: 123}),
src/renderers/dom/test/__tests__/ReactTestUtils-test.js
+5 -1
@@ -14,6 +14,7 @@
14 let createRenderer;
15 let React;
16 let ReactDOM;
17 +let ReactDOMFeatureFlags;
18 let ReactDOMServer;
19 let ReactTestUtils;
20
@@ -24,6 +25,7 @@ describe('ReactTestUtils', () => {
25 ReactDOM = require('react-dom');
26 ReactDOMServer = require('react-dom/server');
27 ReactTestUtils = require('react-dom/test-utils');
28 + ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
29 });
30
31 it('can scryRenderedDOMComponentsWithClass with TextComponent', () => {
@@ -173,7 +175,9 @@ describe('ReactTestUtils', () => {
175
176 const markup = ReactDOMServer.renderToString(<Root />);
177 const testDocument = getTestDocument(markup);
176 - const component = ReactDOM.render(<Root />, testDocument);
178 + const component = ReactDOMFeatureFlags.useFiber
179 + ? ReactDOM.hydrate(<Root />, testDocument)
180 + : ReactDOM.render(<Root />, testDocument);
181
182 expect(component.refs.html.tagName).toBe('HTML');
183 expect(component.refs.head.tagName).toBe('HEAD');
src/renderers/shared/server/ReactPartialRenderer.js
-1
@@ -305,7 +305,6 @@ function createOpenTagMarkup(
305 if (isRootElement) {
306 ret += ' ' + DOMMarkupOperations.createMarkupForRoot();
307 }
308 - ret += ' ' + DOMMarkupOperations.createMarkupForID('');
308 return ret;
309 }
310