@samitouri / QOS-React-2 / commits / 260d90ba02
Fork 0

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;