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: client text</div>,
208
- div,
209
- );
223
+ if (ReactDOMFeatureFlags.useFiber) {
224
+ ReactDOM.hydrate(
225
+ <div>This markup contains an nbsp entity: client text</div>,
226
+ div,
227
+ );
228
+ } else {
229
+ ReactDOM.render(
230
+ <div>This markup contains an nbsp entity: 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=">" ' +
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