Forbid full-page rendering without server rendering
Final part of server rendering cleanup. We should only support full-page rendering when server rendering is involved since otherwise it's slow and can crash browsers when you start adding and removing document roots. This diff removes the magic innerHTML code (since that will be handled by the server rendering piece) and adds the right assertions and errors. Because there's no longer a dangerous of accidentally nuking the whole page, I removed allowFullPageRender which is yet another internal we no longer need to expose.
Pete Hunt committed
Jan 6, 2014 at 09:36 UTC
089a494a1f0cc1892f1525fd746bac10c5a55175
7 files changed
+138
-226
src/core/ReactComponentBrowserEnvironment.js
+21
-9
@@ -27,7 +27,6 @@ var ReactReconcileTransaction = require('ReactReconcileTransaction');
27
28
var getReactRootElementInContainer = require('getReactRootElementInContainer');
29
var invariant = require('invariant');
30
-var mutateHTMLNodeWithMarkup = require('mutateHTMLNodeWithMarkup');
30
31
32
var ELEMENT_NODE_TYPE = 1;
@@ -84,16 +83,29 @@ var ReactComponentBrowserEnvironment = {
83
invariant(
84
container && (
85
container.nodeType === ELEMENT_NODE_TYPE ||
87
- container.nodeType === DOC_NODE_TYPE && ReactMount.allowFullPageRender
86
+ container.nodeType === DOC_NODE_TYPE
87
),
88
'mountComponentIntoNode(...): Target container is not valid.'
89
);
90
+
91
if (shouldReuseMarkup) {
92
if (ReactMarkupChecksum.canReuseMarkup(
93
markup,
94
getReactRootElementInContainer(container))) {
95
return;
96
} else {
97
+ invariant(
98
+ container.nodeType !== DOC_NODE_TYPE,
99
+ 'You\'re trying to render a component to the document using ' +
100
+ 'server rendering but the checksum was invalid. This usually ' +
101
+ 'means you rendered a different component type or props on ' +
102
+ 'the client from the one on the server, or your render() methods ' +
103
+ 'are impure. React cannot handle this case due to cross-browser ' +
104
+ 'quirks by rendering at the document root. You should look for ' +
105
+ 'environment dependent code in your components and ensure ' +
106
+ 'the props are the same client and server side.'
107
+ );
108
+
109
if (__DEV__) {
110
console.warn(
111
'React attempted to use reuse markup in a container but the ' +
@@ -108,13 +120,13 @@ var ReactComponentBrowserEnvironment = {
120
}
121
}
122
111
- // You can't naively set the innerHTML of the entire document. You need
112
- // to mutate documentElement which requires doing some crazy tricks. See
113
- // mutateHTMLNodeWithMarkup()
114
- if (container.nodeType === DOC_NODE_TYPE) {
115
- mutateHTMLNodeWithMarkup(container.documentElement, markup);
116
- return;
117
- }
123
+ invariant(
124
+ container.nodeType !== DOC_NODE_TYPE,
125
+ 'You\'re trying to render a component to the document but ' +
126
+ 'you didn\'t use server rendering. We can\'t do this ' +
127
+ 'without using server rendering due to cross-browser quirks. ' +
128
+ 'See renderComponentToString() for server rendering.'
129
+ );
130
131
// Asynchronously inject markup by ensuring that the container is not in
132
// the document when settings its `innerHTML`.
src/core/ReactMount.js
-5
@@ -179,11 +179,6 @@ function purgeID(id) {
179
* Inside of `container`, the first element rendered is the "reactRoot".
180
*/
181
var ReactMount = {
182
- /**
183
- * Safety guard to prevent accidentally rendering over the entire HTML tree.
184
- */
185
- allowFullPageRender: false,
186
-
182
/** Time spent generating markup. */
183
totalInstantiationTime: 0,
184
src/core/__tests__/ReactRenderDocument-test.js
+108
-52
@@ -57,12 +57,14 @@ describe('rendering React components at document', function() {
57
}
58
});
59
60
- ReactMount.allowFullPageRender = true;
61
- var component = React.renderComponent(<Root />, testDocument);
62
- expect(testDocument.body.innerHTML).toBe(' Hello world ');
60
+ React.renderComponentToString(<Root />, function(markup) {
61
+ testDocument.innerHTML = markup;
62
+ var component = React.renderComponent(<Root />, testDocument);
63
+ expect(testDocument.body.innerHTML).toBe(' Hello world ');
64
64
- var componentID = ReactMount.getReactRootID(testDocument);
65
- expect(componentID).toBe(component._rootNodeID);
65
+ var componentID = ReactMount.getReactRootID(testDocument);
66
+ expect(componentID).toBe(component._rootNodeID);
67
+ });
68
});
69
70
it('should not be able to unmount component from document node', function() {
@@ -83,20 +85,22 @@ describe('rendering React components at document', function() {
85
}
86
});
87
86
- ReactMount.allowFullPageRender = true;
87
- React.renderComponent(<Root />, testDocument);
88
- expect(testDocument.body.innerHTML).toBe(' Hello world ');
89
-
90
- expect(function() {
91
- React.unmountComponentAtNode(testDocument);
92
- }).toThrow(
93
- 'Invariant Violation: ReactFullPageComponenthtml tried to unmount. ' +
94
- 'Because of cross-browser quirks it is impossible to unmount some ' +
95
- 'top-level components (eg <html>, <head>, and <body>) reliably and ' +
96
- 'efficiently. To fix this, have a single top-level component that ' +
97
- 'never unmounts render these elements.'
98
- );
99
- expect(testDocument.body.innerHTML).toBe(' Hello world ');
88
+ React.renderComponentToString(<Root />, function(markup) {
89
+ testDocument.innerHTML = markup;
90
+ React.renderComponent(<Root />, testDocument);
91
+ expect(testDocument.body.innerHTML).toBe(' Hello world ');
92
+
93
+ expect(function() {
94
+ React.unmountComponentAtNode(testDocument);
95
+ }).toThrow(
96
+ 'Invariant Violation: ReactFullPageComponenthtml tried to unmount. ' +
97
+ 'Because of cross-browser quirks it is impossible to unmount some ' +
98
+ 'top-level components (eg <html>, <head>, and <body>) reliably ' +
99
+ 'and efficiently. To fix this, have a single top-level component ' +
100
+ 'that never unmounts render these elements.'
101
+ );
102
+ expect(testDocument.body.innerHTML).toBe(' Hello world ');
103
+ });
104
});
105
106
it('should not be able to switch root constructors', function() {
@@ -132,23 +136,26 @@ describe('rendering React components at document', function() {
136
}
137
});
138
135
- ReactMount.allowFullPageRender = true;
136
- React.renderComponent(<Component />, testDocument);
139
+ React.renderComponentToString(<Component />, function(markup) {
140
+ testDocument.innerHTML = markup;
141
138
- expect(testDocument.body.innerHTML).toBe(' Hello world ');
142
+ React.renderComponent(<Component />, testDocument);
143
140
- // Reactive update
141
- expect(function() {
142
- React.renderComponent(<Component2 />, testDocument);
143
- }).toThrow(
144
- 'Invariant Violation: ReactFullPageComponenthtml tried to unmount. ' +
145
- 'Because of cross-browser quirks it is impossible to unmount some ' +
146
- 'top-level components (eg <html>, <head>, and <body>) reliably and ' +
147
- 'efficiently. To fix this, have a single top-level component that ' +
148
- 'never unmounts render these elements.'
149
- );
144
+ expect(testDocument.body.innerHTML).toBe(' Hello world ');
145
+
146
+ // Reactive update
147
+ expect(function() {
148
+ React.renderComponent(<Component2 />, testDocument);
149
+ }).toThrow(
150
+ 'Invariant Violation: ReactFullPageComponenthtml tried to unmount. ' +
151
+ 'Because of cross-browser quirks it is impossible to unmount some ' +
152
+ 'top-level components (eg <html>, <head>, and <body>) reliably ' +
153
+ 'and efficiently. To fix this, have a single top-level component ' +
154
+ 'that never unmounts render these elements.'
155
+ );
156
151
- expect(testDocument.body.innerHTML).toBe(' Hello world ');
157
+ expect(testDocument.body.innerHTML).toBe(' Hello world ');
158
+ });
159
160
});
161
@@ -169,38 +176,87 @@ describe('rendering React components at document', function() {
176
);
177
}
178
});
172
- ReactMount.allowFullPageRender = true;
173
- React.renderComponent(<Component text="Hello world" />, testDocument);
179
175
- expect(testDocument.body.innerHTML).toBe('Hello world');
180
+ React.renderComponentToString(
181
+ <Component text="Hello world" />,
182
+ function(markup) {
183
+ testDocument.innerHTML = markup;
184
+
185
+ React.renderComponent(<Component text="Hello world" />, testDocument);
186
+
187
+ expect(testDocument.body.innerHTML).toBe('Hello world');
188
+ }
189
+ );
190
});
191
178
- it('should throw on full document render', function() {
192
+ it('should give helpful errors on state desync', function() {
193
expect(testDocument).not.toBeUndefined();
194
181
- var container = testDocument;
182
- expect(function() {
183
- React.renderComponent(<html />, container);
184
- }).toThrow(
185
- 'Invariant Violation: mountComponentIntoNode(...): Target container is ' +
186
- 'not valid.'
195
+ var Component = React.createClass({
196
+ render: function() {
197
+ return (
198
+ <html>
199
+ <head>
200
+ <title>Hello World</title>
201
+ </head>
202
+ <body>
203
+ {this.props.text}
204
+ </body>
205
+ </html>
206
+ );
207
+ }
208
+ });
209
+
210
+ React.renderComponentToString(
211
+ <Component text="Goodbye world" />,
212
+ function(markup) {
213
+ testDocument.innerHTML = markup;
214
+
215
+ expect(function() {
216
+ // Notice the text is different!
217
+ React.renderComponent(<Component text="Hello world" />, testDocument);
218
+ }).toThrow(
219
+ 'Invariant Violation: ' +
220
+ 'You\'re trying to render a component to the document using ' +
221
+ 'server rendering but the checksum was invalid. This usually ' +
222
+ 'means you rendered a different component type or props on ' +
223
+ 'the client from the one on the server, or your render() methods ' +
224
+ 'are impure. React cannot handle this case due to cross-browser ' +
225
+ 'quirks by rendering at the document root. You should look for ' +
226
+ 'environment dependent code in your components and ensure ' +
227
+ 'the props are the same client and server side.'
228
+ );
229
+ }
230
);
188
- ReactMount.allowFullPageRender = true;
189
- expect(function() {
190
- React.renderComponent(<html />, container);
191
- }).not.toThrow();
231
});
232
194
- it('should throw on full document render of non-html', function() {
233
+ it('should throw on full document render w/ no markup', function() {
234
expect(testDocument).not.toBeUndefined();
235
236
var container = testDocument;
198
- ReactMount.allowFullPageRender = true;
237
+
238
+ var Component = React.createClass({
239
+ render: function() {
240
+ return (
241
+ <html>
242
+ <head>
243
+ <title>Hello World</title>
244
+ </head>
245
+ <body>
246
+ {this.props.text}
247
+ </body>
248
+ </html>
249
+ );
250
+ }
251
+ });
252
+
253
expect(function() {
200
- React.renderComponent(<div />, container);
254
+ React.renderComponent(<Component />, container);
255
}).toThrow(
202
- 'Invariant Violation: mutateHTMLNodeWithMarkup(): ' +
203
- 'markup must start with <html'
256
+ 'Invariant Violation: You\'re trying to render a component to the ' +
257
+ 'document but you didn\'t use server rendering. We can\'t do this ' +
258
+ 'without using server rendering due to cross-browser quirks. See ' +
259
+ 'renderComponentToString() for server rendering.'
260
);
261
});
262
src/dom/Danger.js
+8
-7
@@ -27,7 +27,6 @@ var createNodesFromMarkup = require('createNodesFromMarkup');
27
var emptyFunction = require('emptyFunction');
28
var getMarkupWrap = require('getMarkupWrap');
29
var invariant = require('invariant');
30
-var mutateHTMLNodeWithMarkup = require('mutateHTMLNodeWithMarkup');
30
31
var OPEN_TAG_NAME_EXP = /^(<[^ \/>]+)/;
32
var RESULT_INDEX_ATTR = 'data-danger-index';
@@ -171,12 +170,14 @@ var Danger = {
170
'immediately.'
171
);
172
invariant(markup, 'dangerouslyReplaceNodeWithMarkup(...): Missing markup.');
174
- // createNodesFromMarkup() won't work if the markup is rooted by <html>
175
- // since it has special semantic meaning. So we use an alternatie strategy.
176
- if (oldChild.tagName.toLowerCase() === 'html') {
177
- mutateHTMLNodeWithMarkup(oldChild, markup);
178
- return;
179
- }
173
+ invariant(
174
+ oldChild.tagName.toLowerCase() !== 'html',
175
+ 'dangerouslyReplaceNodeWithMarkup(...): Cannot replace markup of the ' +
176
+ '<html> node. This is because browser quirks make this unreliable ' +
177
+ 'and/or slow. If you want to render to the root you must use ' +
178
+ 'server rendering. See renderComponentToString().'
179
+ );
180
+
181
var newChild = createNodesFromMarkup(markup, emptyFunction)[0];
182
oldChild.parentNode.replaceChild(newChild, oldChild);
183
}
src/dom/__tests__/mutateHTMLNodeWithMarkup-test.js
deleted
-52
@@ -1,52 +0,0 @@
1
-/**
2
- * Copyright 2013 Facebook, Inc.
3
- *
4
- * Licensed under the Apache License, Version 2.0 (the "License");
5
- * you may not use this file except in compliance with the License.
6
- * You may obtain a copy of the License at
7
- *
8
- * http://www.apache.org/licenses/LICENSE-2.0
9
- *
10
- * Unless required by applicable law or agreed to in writing, software
11
- * distributed under the License is distributed on an "AS IS" BASIS,
12
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- * See the License for the specific language governing permissions and
14
- * limitations under the License.
15
- *
16
- * @jsx React.DOM
17
- * @emails react-core
18
- */
19
-
20
-/*jslint evil: true */
21
-
22
-"use strict";
23
-
24
-var getTestDocument = require('getTestDocument');
25
-
26
-var mutateHTMLNodeWithMarkup = require('mutateHTMLNodeWithMarkup');
27
-
28
-describe('mutateHTMLNodeWithMarkup', function() {
29
- it('should mutate the document html', function() {
30
- var html = '<html><head><title>test</title></head><body>test</body></html>';
31
- var testDocument = getTestDocument() || document;
32
-
33
- mutateHTMLNodeWithMarkup(testDocument.documentElement, html);
34
- expect(testDocument.body.innerHTML).toBe('test');
35
- });
36
-
37
- it('should change attributes', function() {
38
- var html = '<html><head><title>test</title></head><body>test</body></html>';
39
- var testDocument = getTestDocument() || document;
40
-
41
- mutateHTMLNodeWithMarkup(testDocument.documentElement, html);
42
- expect(!!testDocument.documentElement.getAttribute('lang')).toBe(false);
43
-
44
- var html2 = '<html lang="en"><head><title>test</title></head>' +
45
- '<body>test</body></html>';
46
- mutateHTMLNodeWithMarkup(testDocument.documentElement, html2);
47
- expect(testDocument.documentElement.getAttribute('lang')).toBe('en');
48
-
49
- mutateHTMLNodeWithMarkup(testDocument.documentElement, html);
50
- expect(!!testDocument.documentElement.getAttribute('lang')).toBe(false);
51
- });
52
-});
src/dom/components/createFullPageComponent.js
+1
-1
@@ -47,7 +47,7 @@ function createFullPageComponent(componentClass) {
47
'%s tried to unmount. Because of cross-browser quirks it is ' +
48
'impossible to unmount some top-level components (eg <html>, <head>, ' +
49
' and <body>) reliably and efficiently. To fix this, have a single ' +
50
- ' top-level component that never unmounts render these elements.',
50
+ 'top-level component that never unmounts render these elements.',
51
this.constructor.displayName
52
);
53
},
src/dom/mutateHTMLNodeWithMarkup.js
deleted
-100
@@ -1,100 +0,0 @@
1
-/**
2
- * Copyright 2013 Facebook, Inc.
3
- *
4
- * Licensed under the Apache License, Version 2.0 (the "License");
5
- * you may not use this file except in compliance with the License.
6
- * You may obtain a copy of the License at
7
- *
8
- * http://www.apache.org/licenses/LICENSE-2.0
9
- *
10
- * Unless required by applicable law or agreed to in writing, software
11
- * distributed under the License is distributed on an "AS IS" BASIS,
12
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- * See the License for the specific language governing permissions and
14
- * limitations under the License.
15
- *
16
- * @providesModule mutateHTMLNodeWithMarkup
17
- * @typechecks static-only
18
- */
19
-
20
-/*jslint evil: true */
21
-
22
-'use strict';
23
-
24
-var createNodesFromMarkup = require('createNodesFromMarkup');
25
-var filterAttributes = require('filterAttributes');
26
-var invariant = require('invariant');
27
-
28
-/**
29
- * You can't set the innerHTML of a document. Unless you have
30
- * this function.
31
- *
32
- * @param {DOMElement} node with tagName == 'html'
33
- * @param {string} markup markup string including <html>.
34
- */
35
-function mutateHTMLNodeWithMarkup(node, markup) {
36
- invariant(
37
- node.tagName.toLowerCase() === 'html',
38
- 'mutateHTMLNodeWithMarkup(): node must have tagName of "html", got %s',
39
- node.tagName
40
- );
41
-
42
- markup = markup.trim();
43
- invariant(
44
- markup.toLowerCase().indexOf('<html') === 0,
45
- 'mutateHTMLNodeWithMarkup(): markup must start with <html'
46
- );
47
-
48
- // First let's extract the various pieces of markup.
49
- var htmlOpenTagEnd = markup.indexOf('>') + 1;
50
- var htmlCloseTagStart = markup.lastIndexOf('<');
51
- var htmlOpenTag = markup.substring(0, htmlOpenTagEnd);
52
- var innerHTML = markup.substring(htmlOpenTagEnd, htmlCloseTagStart);
53
-
54
- // Now for the fun stuff. Pass through both sets of attributes and
55
- // bring them up-to-date. We get the new set by creating a markup
56
- // fragment.
57
- var shouldExtractAttributes = htmlOpenTag.indexOf(' ') > -1;
58
- var attributeHolder = null;
59
-
60
- if (shouldExtractAttributes) {
61
- // We extract the attributes by creating a <span> and evaluating
62
- // the node.
63
- attributeHolder = createNodesFromMarkup(
64
- htmlOpenTag.replace('html ', 'span ') + '</span>'
65
- )[0];
66
-
67
- // Add all attributes present in attributeHolder
68
- var attributesToSet = filterAttributes(
69
- attributeHolder,
70
- function(attr) {
71
- return node.getAttributeNS(attr.namespaceURI, attr.name) !== attr.value;
72
- }
73
- );
74
- attributesToSet.forEach(function(attr) {
75
- node.setAttributeNS(attr.namespaceURI, attr.name, attr.value);
76
- });
77
- }
78
-
79
- // Remove all attributes not present in attributeHolder
80
- var attributesToRemove = filterAttributes(
81
- node,
82
- function(attr) {
83
- // Remove all attributes if attributeHolder is null or if it does not have
84
- // the desired attribute.
85
- return !(
86
- attributeHolder &&
87
- attributeHolder.hasAttributeNS(attr.namespaceURI, attr.name)
88
- );
89
- }
90
- );
91
- attributesToRemove.forEach(function(attr) {
92
- node.removeAttributeNS(attr.namespaceURI, attr.name);
93
- });
94
-
95
- // Finally, set the inner HTML. No tricks needed. Do this last to
96
- // minimize likelihood of triggering reflows.
97
- node.innerHTML = innerHTML;
98
-}
99
-
100
-module.exports = mutateHTMLNodeWithMarkup;