@samitouri / QOS-React-2 / commits / cd2aecc377

fix(ServerRendering): execution should be sync

The documentation states that React.renderComponentToString 'uses a callback API to keep the API async', but the implementation is actually synchronous. In order to maintain this contract the callback should always be called asynchronously or be change to a synchronous API. As per the discussion of pull request 982, the API should be changed to a synchronous one.

Ben Ripkens committed Jan 28, 2014 at 19:53 UTC cd2aecc3777d39a8844ab1f72fe6c40d65317872
5 files changed +77 -104
CONTRIBUTING.md
+1
@@ -57,6 +57,7 @@ Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe
57 * `"use strict";`
58 * 80 character line length
59 * "Attractive"
60 +* Do not use the optional parameters of `setTimeout` and `setInterval`
61
62 ## License
63
src/core/__tests__/ReactRenderDocument-test.js
+47 -54
@@ -64,14 +64,13 @@ describe('rendering React components at document', function() {
64 }
65 });
66
67 - React.renderComponentToString(<Root />, function(markup) {
68 - testDocument = getTestDocument(markup);
69 - var component = React.renderComponent(<Root />, testDocument);
70 - expect(testDocument.body.innerHTML).toBe('Hello world');
67 + var markup = React.renderComponentToString(<Root />);
68 + testDocument = getTestDocument(markup);
69 + var component = React.renderComponent(<Root />, testDocument);
70 + expect(testDocument.body.innerHTML).toBe('Hello world');
71
72 - var componentID = ReactMount.getReactRootID(testDocument);
73 - expect(componentID).toBe(component._rootNodeID);
74 - });
72 + var componentID = ReactMount.getReactRootID(testDocument);
73 + expect(componentID).toBe(component._rootNodeID);
74 });
75
76 it('should not be able to unmount component from document node', function() {
@@ -92,17 +91,16 @@ describe('rendering React components at document', function() {
91 }
92 });
93
95 - React.renderComponentToString(<Root />, function(markup) {
96 - testDocument = getTestDocument(markup);
97 - React.renderComponent(<Root />, testDocument);
98 - expect(testDocument.body.innerHTML).toBe('Hello world');
94 + var markup = React.renderComponentToString(<Root />);
95 + testDocument = getTestDocument(markup);
96 + React.renderComponent(<Root />, testDocument);
97 + expect(testDocument.body.innerHTML).toBe('Hello world');
98
100 - expect(function() {
101 - React.unmountComponentAtNode(testDocument);
102 - }).toThrow(UNMOUNT_INVARIANT_MESSAGE);
99 + expect(function() {
100 + React.unmountComponentAtNode(testDocument);
101 + }).toThrow(UNMOUNT_INVARIANT_MESSAGE);
102
104 - expect(testDocument.body.innerHTML).toBe('Hello world');
105 - });
103 + expect(testDocument.body.innerHTML).toBe('Hello world');
104 });
105
106 it('should not be able to switch root constructors', function() {
@@ -138,20 +136,19 @@ describe('rendering React components at document', function() {
136 }
137 });
138
141 - React.renderComponentToString(<Component />, function(markup) {
142 - testDocument = getTestDocument(markup);
139 + var markup = React.renderComponentToString(<Component />);
140 + testDocument = getTestDocument(markup);
141
144 - React.renderComponent(<Component />, testDocument);
142 + React.renderComponent(<Component />, testDocument);
143
146 - expect(testDocument.body.innerHTML).toBe('Hello world');
144 + expect(testDocument.body.innerHTML).toBe('Hello world');
145
148 - // Reactive update
149 - expect(function() {
150 - React.renderComponent(<Component2 />, testDocument);
151 - }).toThrow(UNMOUNT_INVARIANT_MESSAGE);
146 + // Reactive update
147 + expect(function() {
148 + React.renderComponent(<Component2 />, testDocument);
149 + }).toThrow(UNMOUNT_INVARIANT_MESSAGE);
150
153 - expect(testDocument.body.innerHTML).toBe('Hello world');
154 - });
151 + expect(testDocument.body.innerHTML).toBe('Hello world');
152 });
153
154 it('should be able to mount into document', function() {
@@ -172,16 +169,14 @@ describe('rendering React components at document', function() {
169 }
170 });
171
175 - React.renderComponentToString(
176 - <Component text="Hello world" />,
177 - function(markup) {
178 - testDocument = getTestDocument(markup);
172 + var markup = React.renderComponentToString(
173 + <Component text="Hello world" />
174 + );
175 + testDocument = getTestDocument(markup);
176
180 - React.renderComponent(<Component text="Hello world" />, testDocument);
177 + React.renderComponent(<Component text="Hello world" />, testDocument);
178
182 - expect(testDocument.body.innerHTML).toBe('Hello world');
183 - }
184 - );
179 + expect(testDocument.body.innerHTML).toBe('Hello world');
180 });
181
182 it('should give helpful errors on state desync', function() {
@@ -202,26 +197,24 @@ describe('rendering React components at document', function() {
197 }
198 });
199
205 - React.renderComponentToString(
206 - <Component text="Goodbye world" />,
207 - function(markup) {
208 - testDocument = getTestDocument(markup);
209 -
210 - expect(function() {
211 - // Notice the text is different!
212 - React.renderComponent(<Component text="Hello world" />, testDocument);
213 - }).toThrow(
214 - 'Invariant Violation: ' +
215 - 'You\'re trying to render a component to the document using ' +
216 - 'server rendering but the checksum was invalid. This usually ' +
217 - 'means you rendered a different component type or props on ' +
218 - 'the client from the one on the server, or your render() methods ' +
219 - 'are impure. React cannot handle this case due to cross-browser ' +
220 - 'quirks by rendering at the document root. You should look for ' +
221 - 'environment dependent code in your components and ensure ' +
222 - 'the props are the same client and server side.'
223 - );
224 - }
200 + var markup = React.renderComponentToString(
201 + <Component text="Goodbye world" />
202 + );
203 + testDocument = getTestDocument(markup);
204 +
205 + expect(function() {
206 + // Notice the text is different!
207 + React.renderComponent(<Component text="Hello world" />, testDocument);
208 + }).toThrow(
209 + 'Invariant Violation: ' +
210 + 'You\'re trying to render a component to the document using ' +
211 + 'server rendering but the checksum was invalid. This usually ' +
212 + 'means you rendered a different component type or props on ' +
213 + 'the client from the one on the server, or your render() methods ' +
214 + 'are impure. React cannot handle this case due to cross-browser ' +
215 + 'quirks by rendering at the document root. You should look for ' +
216 + 'environment dependent code in your components and ensure ' +
217 + 'the props are the same client and server side.'
218 );
219 });
220
src/core/__tests__/ReactTextComponent-test.js
+3 -4
@@ -30,9 +30,8 @@ describe('ReactTextComponent', function() {
30 var ThisThingShouldBeEscaped = '">>> LULZ <<<"';
31 var ThisThingWasBeEscaped = '&quot;&gt;&gt;&gt; LULZ &lt;&lt;&lt;&quot;';
32 var thing = React.DOM.div(null, React.DOM.span({key:ThisThingShouldBeEscaped}, ["LULZ"]));
33 - React.renderComponentToString(thing, function(html){
34 - expect(html).not.toContain(ThisThingShouldBeEscaped);
35 - expect(html).toContain(ThisThingWasBeEscaped);
36 - });
33 + var html = React.renderComponentToString(thing);
34 + expect(html).not.toContain(ThisThingShouldBeEscaped);
35 + expect(html).toContain(ThisThingWasBeEscaped);
36 })
37 });
src/environment/ReactServerRendering.js
+7 -10
@@ -27,30 +27,27 @@ var invariant = require('invariant');
27
28 /**
29 * @param {ReactComponent} component
30 - * @param {function} callback
30 + * @return {String} the markup
31 */
32 -function renderComponentToString(component, callback) {
33 - // We use a callback API to keep the API async in case in the future we ever
34 - // need it, but in reality this is a synchronous operation.
35 -
32 +function renderComponentToString(component) {
33 invariant(
34 ReactComponent.isValidComponent(component),
35 'renderComponentToString(): You must pass a valid ReactComponent.'
36 );
37
38 invariant(
42 - typeof callback === 'function',
43 - 'renderComponentToString(): You must pass a function as a callback.'
39 + !(arguments.length === 2 && typeof arguments[1] === 'function'),
40 + 'renderComponentToString(): This function became synchronous and now ' +
41 + 'returns the generated markup. Please remove the second parameter.'
42 );
43
44 var id = ReactInstanceHandles.createReactRootID();
45 var transaction = ReactReconcileTransaction.getPooled();
46 transaction.reinitializeTransaction();
47 try {
50 - transaction.perform(function() {
48 + return transaction.perform(function() {
49 var markup = component.mountComponent(id, transaction, 0);
52 - markup = ReactMarkupChecksum.addChecksumToMarkup(markup);
53 - callback(markup);
50 + return ReactMarkupChecksum.addChecksumToMarkup(markup);
51 }, null);
52 } finally {
53 ReactReconcileTransaction.release(transaction);
src/environment/__tests__/ReactServerRendering-test.js
+19 -36
@@ -56,12 +56,8 @@ describe('ReactServerRendering', function() {
56 });
57
58 it('should generate simple markup', function() {
59 - var response;
60 - ReactServerRendering.renderComponentToString(
61 - <span>hello world</span>,
62 - function(response_) {
63 - response = response_;
64 - }
59 + var response = ReactServerRendering.renderComponentToString(
60 + <span>hello world</span>
61 );
62 expect(response).toMatch(
63 '<span ' + ID_ATTRIBUTE_NAME + '="[^"]+" ' +
@@ -73,12 +69,10 @@ describe('ReactServerRendering', function() {
69 var EventPluginHub = require('EventPluginHub');
70 var cb = mocks.getMockFunction();
71
76 - ReactServerRendering.renderComponentToString(
77 - <span onClick={cb}>hello world</span>,
78 - function() {
79 - expect(EventPluginHub.__getListenerBank()).toEqual({});
80 - }
72 + var response = ReactServerRendering.renderComponentToString(
73 + <span onClick={cb}>hello world</span>
74 );
75 + expect(EventPluginHub.__getListenerBank()).toEqual({});
76 });
77
78 it('should render composite components', function() {
@@ -92,12 +86,8 @@ describe('ReactServerRendering', function() {
86 return <span>My name is {this.props.name}</span>;
87 }
88 });
95 - var response;
96 - ReactServerRendering.renderComponentToString(
97 - <Parent />,
98 - function(response_) {
99 - response = response_;
100 - }
89 + var response = ReactServerRendering.renderComponentToString(
90 + <Parent />
91 );
92 expect(response).toMatch(
93 '<div ' + ID_ATTRIBUTE_NAME + '="[^"]+" ' +
@@ -144,13 +134,8 @@ describe('ReactServerRendering', function() {
134 }
135 });
136
147 - var response;
148 -
149 - ReactServerRendering.renderComponentToString(
150 - <TestComponent />,
151 - function (_response) {
152 - response = _response;
153 - }
137 + var response = ReactServerRendering.renderComponentToString(
138 + <TestComponent />
139 );
140
141 expect(response).toMatch(
@@ -212,14 +197,10 @@ describe('ReactServerRendering', function() {
197 expect(element.innerHTML).toEqual('');
198
199 ExecutionEnvironment.canUseDOM = false;
215 - ReactServerRendering.renderComponentToString(
216 - <TestComponent name="x" />,
217 - function(markup) {
218 - lastMarkup = markup;
219 - }
200 + lastMarkup = ReactServerRendering.renderComponentToString(
201 + <TestComponent name="x" />
202 );
203 ExecutionEnvironment.canUseDOM = true;
222 -
204 element.innerHTML = lastMarkup + ' __sentinel__';
205
206 React.renderComponent(<TestComponent name="x" />, element);
@@ -250,23 +231,25 @@ describe('ReactServerRendering', function() {
231 expect(
232 ReactServerRendering.renderComponentToString.bind(
233 ReactServerRendering,
253 - 'not a component',
254 - function() {}
234 + 'not a component'
235 )
236 ).toThrow(
237 'Invariant Violation: renderComponentToString(): You must pass ' +
238 'a valid ReactComponent.'
239 );
240 + });
241
242 + it('should provide guidance for breaking API changes', function() {
243 expect(
244 ReactServerRendering.renderComponentToString.bind(
245 ReactServerRendering,
264 - React.DOM.div(),
265 - 'not a function'
246 + <div />,
247 + function(){}
248 )
249 ).toThrow(
268 - 'Invariant Violation: renderComponentToString(): You must pass ' +
269 - 'a function as a callback.'
250 + 'Invariant Violation: renderComponentToString(): This function became ' +
251 + 'synchronous and now returns the generated markup. Please remove the ' +
252 + 'second parameter.'
253 );
254 });
255 });