The library for web and native user interfaces.
Warn when server-rendered markup is not what we expect on the client
As @leebyron and balpert pointed out, if the markup on the server is differnet than what the client expects undefined behavior and chaos may ensue. A good fallback is for us to just inject the client-side markup (as it is the source of truth) and warn the user in __DEV__ that something is wrong. In order to do a fast browser-independent check of the DOM I use an adler32 checksum of the generated markup. I believe this is better than a simple innerHTML compare because different browsers massage innerHTML differently.
Pete Hunt committed
Jul 24, 2013 at 17:19 UTC
260d90ba029483c4d2bca77ad89abf8d16e7d545
7 files changed
+186
-23
src/core/ReactComponent.js
+19
-1
@@ -20,9 +20,11 @@
20
21
"use strict";
22
23
+var getReactRootElementInContainer = require('getReactRootElementInContainer');
24
var ReactCurrentOwner = require('ReactCurrentOwner');
25
var ReactDOMIDOperations = require('ReactDOMIDOperations');
26
var ReactID = require('ReactID');
27
+var ReactMarkupChecksum = require('ReactMarkupChecksum');
28
var ReactOwner = require('ReactOwner');
29
var ReactReconcileTransaction = require('ReactReconcileTransaction');
30
var ReactUpdates = require('ReactUpdates');
@@ -500,7 +502,23 @@ var ReactComponent = {
502
var markup = this.mountComponent(rootID, transaction);
503
504
if (shouldReuseMarkup) {
503
- return;
505
+ if (ReactMarkupChecksum.canReuseMarkup(
506
+ markup,
507
+ getReactRootElementInContainer(container))) {
508
+ return;
509
+ } else {
510
+ if (__DEV__) {
511
+ console.warn(
512
+ 'React attempted to use reuse markup in a container but the ' +
513
+ 'checksum was invalid. This generally means that you are using ' +
514
+ 'server rendering and the markup generated on the server was ' +
515
+ 'not what the client was expecting. React injected new markup ' +
516
+ 'to compensate which works but you have lost many of the ' +
517
+ 'benefits of server rendering. Instead, figure out why the ' +
518
+ 'markup being generated is different on the client or server.'
519
+ );
520
+ }
521
+ }
522
}
523
524
// Asynchronously inject markup by ensuring that the container is not in
src/core/ReactMount.js
+1
-9
@@ -19,6 +19,7 @@
19
"use strict";
20
21
var invariant = require('invariant');
22
+var getReactRootElementInContainer = require('getReactRootElementInContainer');
23
var ReactEventEmitter = require('ReactEventEmitter');
24
var ReactInstanceHandles = require('ReactInstanceHandles');
25
var ReactEventTopLevelCallback = require('ReactEventTopLevelCallback');
@@ -37,14 +38,6 @@ if (__DEV__) {
38
var rootElementsByReactRootID = {};
39
}
40
40
-/**
41
- * @param {DOMElement} container DOM element that may contain a React component
42
- * @return {?*} DOM element that may have the reactRoot ID, or null.
43
- */
44
-function getReactRootElementInContainer(container) {
45
- return container && container.firstChild;
46
-}
47
-
41
/**
42
* @param {DOMElement} container DOM element that may contain a React component.
43
* @return {?string} A "reactRoot" ID, if a React component is rendered.
@@ -338,7 +331,6 @@ var ReactMount = {
331
var reactRoot = ReactMount.findReactContainerForID(id);
332
return ReactInstanceHandles.findComponentRoot(reactRoot, id);
333
}
341
-
334
};
335
336
module.exports = ReactMount;
src/core/getReactRootElementInContainer.js
new
+29
@@ -0,0 +1,29 @@
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 getReactRootElementInContainer
17
+ */
18
+
19
+"use strict";
20
+
21
+/**
22
+ * @param {DOMElement} container DOM element that may contain a React component
23
+ * @return {?*} DOM element that may have the reactRoot ID, or null.
24
+ */
25
+function getReactRootElementInContainer(container) {
26
+ return container && container.firstChild;
27
+}
28
+
29
+module.exports = getReactRootElementInContainer;
src/environment/ReactMarkupChecksum.js
new
+53
@@ -0,0 +1,53 @@
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 ReactMarkupChecksum
17
+ */
18
+
19
+"use strict";
20
+
21
+var adler32 = require('adler32');
22
+
23
+var ReactMarkupChecksum = {
24
+ CHECKSUM_ATTR_NAME: 'data-react-checksum',
25
+
26
+ /**
27
+ * @param {string} markup Markup string
28
+ * @return {string} Markup string with checksum attribute attached
29
+ */
30
+ addChecksumToMarkup: function(markup) {
31
+ var checksum = adler32(markup);
32
+ return markup.replace(
33
+ '>',
34
+ ' ' + ReactMarkupChecksum.CHECKSUM_ATTR_NAME + '="' + checksum + '">'
35
+ );
36
+ },
37
+
38
+ /**
39
+ * @param {string} markup to use
40
+ * @param {DOMElement} element root React element
41
+ * @returns {boolean} whether or not the markup is the same
42
+ */
43
+ canReuseMarkup: function(markup, element) {
44
+ var existingChecksum = element.getAttribute(
45
+ ReactMarkupChecksum.CHECKSUM_ATTR_NAME
46
+ );
47
+ existingChecksum = existingChecksum && parseInt(existingChecksum, 10);
48
+ var markupChecksum = adler32(markup);
49
+ return markupChecksum === existingChecksum;
50
+ }
51
+};
52
+
53
+module.exports = ReactMarkupChecksum;
src/environment/ReactServerRendering.js
+4
-1
@@ -18,6 +18,7 @@
18
*/
19
"use strict";
20
21
+var ReactMarkupChecksum = require('ReactMarkupChecksum');
22
var ReactReconcileTransaction = require('ReactReconcileTransaction');
23
var ReactInstanceHandles = require('ReactInstanceHandles');
24
@@ -33,7 +34,9 @@ function renderComponentToString(component, callback) {
34
transaction.reinitializeTransaction();
35
try {
36
transaction.perform(function() {
36
- callback(component.mountComponent(id, transaction));
37
+ var markup = component.mountComponent(id, transaction);
38
+ markup = ReactMarkupChecksum.addChecksumToMarkup(markup);
39
+ callback(markup);
40
}, null);
41
} finally {
42
ReactReconcileTransaction.release(transaction);
src/environment/__tests__/ReactServerRendering-test.js
+43
-12
@@ -26,12 +26,16 @@ require('mock-modules')
26
.dontMock('React')
27
.dontMock('ReactID')
28
.dontMock('ReactServerRendering')
29
- .dontMock('ReactTestUtils');
29
+ .dontMock('ReactTestUtils')
30
+ .dontMock('ReactMarkupChecksum');
31
+
32
+var mocks = require('mocks');
33
34
var React;
35
var ReactID;
36
var ReactTestUtils;
37
var ReactServerRendering;
38
+var ReactMarkupChecksum;
39
var ExecutionEnvironment;
40
41
describe('ReactServerRendering', function() {
@@ -43,6 +47,7 @@ describe('ReactServerRendering', function() {
47
ExecutionEnvironment = require('ExecutionEnvironment');
48
ExecutionEnvironment.canUseDOM = false;
49
ReactServerRendering = require('ReactServerRendering');
50
+ ReactMarkupChecksum = require('ReactMarkupChecksum');
51
});
52
53
it('should generate simple markup', function() {
@@ -54,7 +59,8 @@ describe('ReactServerRendering', function() {
59
}
60
);
61
expect(response).toMatch(
57
- '<span ' + ReactID.ATTR_NAME + '="[^"]+">hello world</span>'
62
+ '<span ' + ReactID.ATTR_NAME + '="[^"]+" ' +
63
+ ReactMarkupChecksum.CHECKSUM_ATTR_NAME + '="[^"]+">hello world</span>'
64
);
65
});
66
@@ -77,7 +83,8 @@ describe('ReactServerRendering', function() {
83
}
84
);
85
expect(response).toMatch(
80
- '<div ' + ReactID.ATTR_NAME + '="[^"]+">' +
86
+ '<div ' + ReactID.ATTR_NAME + '="[^"]+" ' +
87
+ ReactMarkupChecksum.CHECKSUM_ATTR_NAME + '="[^"]+">' +
88
'<span ' + ReactID.ATTR_NAME + '="[^"]+">' +
89
'<span ' + ReactID.ATTR_NAME + '="[^"]+">My name is </span>' +
90
'<span ' + ReactID.ATTR_NAME + '="[^"]+">child</span>' +
@@ -130,7 +137,8 @@ describe('ReactServerRendering', function() {
137
);
138
139
expect(response).toMatch(
133
- '<span ' + ReactID.ATTR_NAME + '="[^"]+">' +
140
+ '<span ' + ReactID.ATTR_NAME + '="[^"]+" ' +
141
+ ReactMarkupChecksum.CHECKSUM_ATTR_NAME + '="[^"]+">' +
142
'<span ' + ReactID.ATTR_NAME + '="[^"]+">Component name: </span>' +
143
'<span ' + ReactID.ATTR_NAME + '="[^"]+">TestComponent</span>' +
144
'</span>'
@@ -148,7 +156,7 @@ describe('ReactServerRendering', function() {
156
var numClicks = 0;
157
158
var TestComponent = React.createClass({
151
- componentWillMount: function() {
159
+ componentDidMount: function() {
160
mountCount++;
161
},
162
click: function() {
@@ -179,18 +187,41 @@ describe('ReactServerRendering', function() {
187
expect(mountCount).toEqual(2);
188
expect(element.innerHTML).not.toEqual(lastMarkup);
189
182
- // Now kill the node and render it on top of the old markup, as if
190
+ // Now kill the node and render it on top of server-rendered markup, as if
191
// we used server rendering. We should mount again, but the markup should be
184
- // unchanged.
185
- lastMarkup = element.innerHTML;
192
+ // unchanged. We will append a sentinel at the end of innerHTML to be sure
193
+ // that innerHTML was not changed.
194
+ React.unmountAndReleaseReactRootNode(element);
195
+ expect(element.innerHTML).toEqual('');
196
+
197
+ ExecutionEnvironment.canUseDOM = false;
198
+ ReactServerRendering.renderComponentToString(
199
+ <TestComponent name="x" />,
200
+ function(markup) {
201
+ lastMarkup = markup;
202
+ }
203
+ );
204
+ ExecutionEnvironment.canUseDOM = true;
205
+
206
+ element.innerHTML = lastMarkup + ' __sentinel__';
207
+
208
+ React.renderComponent(<TestComponent name="x" />, element);
209
+ expect(mountCount).toEqual(3);
210
+ expect(element.innerHTML.indexOf('__sentinel__') > -1).toBe(true);
211
React.unmountAndReleaseReactRootNode(element);
212
expect(element.innerHTML).toEqual('');
213
+
214
+ // Now simulate a situation where the app is not idempotent. React should
215
+ // warn but do the right thing.
216
+ var _warn = console.warn;
217
+ console.warn = mocks.getMockFunction();
218
element.innerHTML = lastMarkup;
189
- // NOTE: we pass a different name here. This is to ensure that the markup
190
- // being generated is not replaced.
219
var instance = React.renderComponent(<TestComponent name="y" />, element);
192
- expect(mountCount).toEqual(3);
193
- expect(element.innerHTML).toEqual(lastMarkup);
220
+ expect(mountCount).toEqual(4);
221
+ expect(console.warn.mock.calls.length).toBe(1);
222
+ expect(element.innerHTML.length > 0).toBe(true);
223
+ expect(element.innerHTML).not.toEqual(lastMarkup);
224
+ console.warn = _warn;
225
226
// Ensure the events system works
227
expect(numClicks).toEqual(0);
src/utils/adler32.js
new
+37
@@ -0,0 +1,37 @@
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 adler32
17
+ */
18
+
19
+"use strict";
20
+
21
+var MOD = 65521;
22
+
23
+// This is a clean-room implementation of adler32 designed for detecting
24
+// if markup is not what we expect it to be. It does not need to be
25
+// cryptographically strong, only reasonable good at detecting if markup
26
+// generated on the server is different than that on the client.
27
+function adler32(data) {
28
+ var a = 1;
29
+ var b = 0;
30
+ for (var i = 0; i < data.length; i++) {
31
+ a = (a + data.charCodeAt(i)) % MOD;
32
+ b = (b + a) % MOD;
33
+ }
34
+ return a | (b << 16);
35
+}
36
+
37
+module.exports = adler32;