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 = '">>> LULZ <<<"';
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
});