Refactor rendering to string without checksum & React ID
Finalize API for rendering to static markup. Instead of passing a boolean option to `renderComponentToString`, just use another method.
Cheng Lou committed
Mar 3, 2014 at 09:40 UTC
99dab49f92fe0785c1643ede5b0e7a90cbe1e1e0
3 files changed
+301
-213
src/browser/React.js
+2
@@ -60,6 +60,8 @@ var React = {
60
ReactMount.renderComponent
61
),
62
renderComponentToString: ReactServerRendering.renderComponentToString,
63
+ renderComponentToStaticMarkup:
64
+ ReactServerRendering.renderComponentToStaticMarkup,
65
unmountComponentAtNode: ReactMount.unmountComponentAtNode,
66
isValidClass: ReactCompositeComponent.isValidClass,
67
isValidComponent: ReactComponent.isValidComponent,
src/browser/server/ReactServerRendering.js
+33
-19
@@ -29,10 +29,9 @@ var invariant = require('invariant');
29
30
/**
31
* @param {ReactComponent} component
32
- * @param {?object} options pass a boolean for the key `staticMarkup`. If true,
33
- * the React ID and checksum won't be rendered. Used for rendering static pages.
32
+ * @return {string} the HTML markup
33
*/
35
-function renderComponentToString(component, options) {
34
+function renderComponentToString(component) {
35
invariant(
36
ReactComponent.isValidComponent(component),
37
'renderComponentToString(): You must pass a valid ReactComponent.'
@@ -44,30 +43,45 @@ function renderComponentToString(component, options) {
43
'returns the generated markup. Please remove the second parameter.'
44
);
45
47
- options = options || {};
48
-
46
var transaction;
47
try {
51
- if (options.staticMarkup) {
52
- transaction = ReactServerRenderingTransaction.getPooled(true);
48
+ var id = ReactInstanceHandles.createReactRootID();
49
+ transaction = ReactServerRenderingTransaction.getPooled(false);
50
+
51
+ return transaction.perform(function() {
52
+ var markup = component.mountComponent(id, transaction, 0);
53
+ return ReactMarkupChecksum.addChecksumToMarkup(markup);
54
+ }, null);
55
+ } finally {
56
+ ReactServerRenderingTransaction.release(transaction);
57
+ }
58
+}
59
54
- return transaction.perform(function() {
55
- return component.mountComponent(id, transaction, 0);
56
- }, null);
57
- } else {
58
- var id = ReactInstanceHandles.createReactRootID();
59
- transaction = ReactServerRenderingTransaction.getPooled(false);
60
+/**
61
+ * @param {ReactComponent} component
62
+ * @return {string} the HTML markup, without the extra React ID and checksum
63
+* (for generating static pages)
64
+ */
65
+function renderComponentToStaticMarkup(component) {
66
+ invariant(
67
+ ReactComponent.isValidComponent(component),
68
+ 'renderComponentToStaticMarkup(): You must pass a valid ReactComponent.'
69
+ );
70
+
71
+ var transaction;
72
+ try {
73
+ var id = ReactInstanceHandles.createReactRootID();
74
+ transaction = ReactServerRenderingTransaction.getPooled(true);
75
61
- return transaction.perform(function() {
62
- var markup = component.mountComponent(id, transaction, 0);
63
- return ReactMarkupChecksum.addChecksumToMarkup(markup);
64
- }, null);
65
- }
76
+ return transaction.perform(function() {
77
+ return component.mountComponent(id, transaction, 0);
78
+ }, null);
79
} finally {
80
ReactServerRenderingTransaction.release(transaction);
81
}
82
}
83
84
module.exports = {
72
- renderComponentToString: renderComponentToString
85
+ renderComponentToString: renderComponentToString,
86
+ renderComponentToStaticMarkup: renderComponentToStaticMarkup
87
};
src/browser/server/__tests__/ReactServerRendering-test.js
+266
-194
@@ -55,214 +55,216 @@ describe('ReactServerRendering', function() {
55
ID_ATTRIBUTE_NAME = DOMProperty.ID_ATTRIBUTE_NAME;
56
});
57
58
- it('should generate simple markup', function() {
59
- var response = ReactServerRendering.renderComponentToString(
60
- <span>hello world</span>
61
- );
62
- expect(response).toMatch(
63
- '<span ' + ID_ATTRIBUTE_NAME + '="[^"]+" ' +
64
- ReactMarkupChecksum.CHECKSUM_ATTR_NAME + '="[^"]+">hello world</span>'
65
- );
66
- });
67
-
68
- it('should not register event listeners', function() {
69
- var EventPluginHub = require('EventPluginHub');
70
- var cb = mocks.getMockFunction();
58
+ describe('renderComponentToString', function() {
59
+ it('should generate simple markup', function() {
60
+ var response = ReactServerRendering.renderComponentToString(
61
+ <span>hello world</span>
62
+ );
63
+ expect(response).toMatch(
64
+ '<span ' + ID_ATTRIBUTE_NAME + '="[^"]+" ' +
65
+ ReactMarkupChecksum.CHECKSUM_ATTR_NAME + '="[^"]+">hello world</span>'
66
+ );
67
+ });
68
72
- var response = ReactServerRendering.renderComponentToString(
73
- <span onClick={cb}>hello world</span>
74
- );
75
- expect(EventPluginHub.__getListenerBank()).toEqual({});
76
- });
69
+ it('should not register event listeners', function() {
70
+ var EventPluginHub = require('EventPluginHub');
71
+ var cb = mocks.getMockFunction();
72
78
- it('should render composite components', function() {
79
- var Parent = React.createClass({
80
- render: function() {
81
- return <div><Child name="child" /></div>;
82
- }
83
- });
84
- var Child = React.createClass({
85
- render: function() {
86
- return <span>My name is {this.props.name}</span>;
87
- }
73
+ ReactServerRendering.renderComponentToString(
74
+ <span onClick={cb}>hello world</span>
75
+ );
76
+ expect(EventPluginHub.__getListenerBank()).toEqual({});
77
});
89
- var response = ReactServerRendering.renderComponentToString(
90
- <Parent />
91
- );
92
- expect(response).toMatch(
93
- '<div ' + ID_ATTRIBUTE_NAME + '="[^"]+" ' +
94
- ReactMarkupChecksum.CHECKSUM_ATTR_NAME + '="[^"]+">' +
95
- '<span ' + ID_ATTRIBUTE_NAME + '="[^"]+">' +
96
- '<span ' + ID_ATTRIBUTE_NAME + '="[^"]+">My name is </span>' +
97
- '<span ' + ID_ATTRIBUTE_NAME + '="[^"]+">child</span>' +
98
- '</span>' +
99
- '</div>'
100
- );
101
- });
78
103
- it('should only execute certain lifecycle methods', function() {
104
- function runTest() {
105
- var lifecycle = [];
106
- var TestComponent = React.createClass({
107
- componentWillMount: function() {
108
- lifecycle.push('componentWillMount');
109
- },
110
- componentDidMount: function() {
111
- lifecycle.push('componentDidMount');
112
- },
113
- getInitialState: function() {
114
- lifecycle.push('getInitialState');
115
- return {name: 'TestComponent'};
116
- },
79
+ it('should render composite components', function() {
80
+ var Parent = React.createClass({
81
render: function() {
118
- lifecycle.push('render');
119
- return <span>Component name: {this.state.name}</span>;
120
- },
121
- componentWillUpdate: function() {
122
- lifecycle.push('componentWillUpdate');
123
- },
124
- componentDidUpdate: function() {
125
- lifecycle.push('componentDidUpdate');
126
- },
127
- shouldComponentUpdate: function() {
128
- lifecycle.push('shouldComponentUpdate');
129
- },
130
- componentWillReceiveProps: function() {
131
- lifecycle.push('componentWillReceiveProps');
132
- },
133
- componentWillUnmount: function() {
134
- lifecycle.push('componentWillUnmount');
82
+ return <div><Child name="child" /></div>;
83
+ }
84
+ });
85
+ var Child = React.createClass({
86
+ render: function() {
87
+ return <span>My name is {this.props.name}</span>;
88
}
89
});
137
-
90
var response = ReactServerRendering.renderComponentToString(
139
- <TestComponent />
91
+ <Parent />
92
);
141
-
93
expect(response).toMatch(
143
- '<span ' + ID_ATTRIBUTE_NAME + '="[^"]+" ' +
94
+ '<div ' + ID_ATTRIBUTE_NAME + '="[^"]+" ' +
95
ReactMarkupChecksum.CHECKSUM_ATTR_NAME + '="[^"]+">' +
145
- '<span ' + ID_ATTRIBUTE_NAME + '="[^"]+">Component name: </span>' +
146
- '<span ' + ID_ATTRIBUTE_NAME + '="[^"]+">TestComponent</span>' +
147
- '</span>'
96
+ '<span ' + ID_ATTRIBUTE_NAME + '="[^"]+">' +
97
+ '<span ' + ID_ATTRIBUTE_NAME + '="[^"]+">My name is </span>' +
98
+ '<span ' + ID_ATTRIBUTE_NAME + '="[^"]+">child</span>' +
99
+ '</span>' +
100
+ '</div>'
101
);
149
- expect(lifecycle).toEqual(
150
- ['getInitialState', 'componentWillMount', 'render']
151
- );
152
- }
153
-
154
- runTest();
102
+ });
103
156
- // This should work the same regardless of whether you can use DOM or not.
157
- ExecutionEnvironment.canUseDOM = true;
158
- runTest();
159
- });
104
+ it('should only execute certain lifecycle methods', function() {
105
+ function runTest() {
106
+ var lifecycle = [];
107
+ var TestComponent = React.createClass({
108
+ componentWillMount: function() {
109
+ lifecycle.push('componentWillMount');
110
+ },
111
+ componentDidMount: function() {
112
+ lifecycle.push('componentDidMount');
113
+ },
114
+ getInitialState: function() {
115
+ lifecycle.push('getInitialState');
116
+ return {name: 'TestComponent'};
117
+ },
118
+ render: function() {
119
+ lifecycle.push('render');
120
+ return <span>Component name: {this.state.name}</span>;
121
+ },
122
+ componentWillUpdate: function() {
123
+ lifecycle.push('componentWillUpdate');
124
+ },
125
+ componentDidUpdate: function() {
126
+ lifecycle.push('componentDidUpdate');
127
+ },
128
+ shouldComponentUpdate: function() {
129
+ lifecycle.push('shouldComponentUpdate');
130
+ },
131
+ componentWillReceiveProps: function() {
132
+ lifecycle.push('componentWillReceiveProps');
133
+ },
134
+ componentWillUnmount: function() {
135
+ lifecycle.push('componentWillUnmount');
136
+ }
137
+ });
138
+
139
+ var response = ReactServerRendering.renderComponentToString(
140
+ <TestComponent />
141
+ );
142
161
- it('should have the correct mounting behavior', function() {
162
- // This test is testing client-side behavior.
163
- ExecutionEnvironment.canUseDOM = true;
164
-
165
- var mountCount = 0;
166
- var numClicks = 0;
167
-
168
- var TestComponent = React.createClass({
169
- componentDidMount: function() {
170
- mountCount++;
171
- },
172
- click: function() {
173
- numClicks++;
174
- },
175
- render: function() {
176
- return (
177
- <span ref="span" onClick={this.click}>Name: {this.props.name}</span>
143
+ expect(response).toMatch(
144
+ '<span ' + ID_ATTRIBUTE_NAME + '="[^"]+" ' +
145
+ ReactMarkupChecksum.CHECKSUM_ATTR_NAME + '="[^"]+">' +
146
+ '<span ' + ID_ATTRIBUTE_NAME + '="[^"]+">Component name: </span>' +
147
+ '<span ' + ID_ATTRIBUTE_NAME + '="[^"]+">TestComponent</span>' +
148
+ '</span>'
149
+ );
150
+ expect(lifecycle).toEqual(
151
+ ['getInitialState', 'componentWillMount', 'render']
152
);
153
}
180
- });
154
182
- var element = document.createElement('div');
183
- React.renderComponent(<TestComponent />, element);
155
+ runTest();
156
185
- var lastMarkup = element.innerHTML;
157
+ // This should work the same regardless of whether you can use DOM or not.
158
+ ExecutionEnvironment.canUseDOM = true;
159
+ runTest();
160
+ });
161
187
- // Exercise the update path. Markup should not change,
188
- // but some lifecycle methods should be run again.
189
- React.renderComponent(<TestComponent name="x" />, element);
190
- expect(mountCount).toEqual(1);
162
+ it('should have the correct mounting behavior', function() {
163
+ // This test is testing client-side behavior.
164
+ ExecutionEnvironment.canUseDOM = true;
165
192
- // Unmount and remount. We should get another mount event and
193
- // we should get different markup, as the IDs are unique each time.
194
- React.unmountComponentAtNode(element);
195
- expect(element.innerHTML).toEqual('');
196
- React.renderComponent(<TestComponent name="x" />, element);
197
- expect(mountCount).toEqual(2);
198
- expect(element.innerHTML).not.toEqual(lastMarkup);
166
+ var mountCount = 0;
167
+ var numClicks = 0;
168
200
- // Now kill the node and render it on top of server-rendered markup, as if
201
- // we used server rendering. We should mount again, but the markup should be
202
- // unchanged. We will append a sentinel at the end of innerHTML to be sure
203
- // that innerHTML was not changed.
204
- React.unmountComponentAtNode(element);
205
- expect(element.innerHTML).toEqual('');
169
+ var TestComponent = React.createClass({
170
+ componentDidMount: function() {
171
+ mountCount++;
172
+ },
173
+ click: function() {
174
+ numClicks++;
175
+ },
176
+ render: function() {
177
+ return (
178
+ <span ref="span" onClick={this.click}>Name: {this.props.name}</span>
179
+ );
180
+ }
181
+ });
182
207
- ExecutionEnvironment.canUseDOM = false;
208
- lastMarkup = ReactServerRendering.renderComponentToString(
209
- <TestComponent name="x" />
210
- );
211
- ExecutionEnvironment.canUseDOM = true;
212
- element.innerHTML = lastMarkup + ' __sentinel__';
213
-
214
- React.renderComponent(<TestComponent name="x" />, element);
215
- expect(mountCount).toEqual(3);
216
- expect(element.innerHTML.indexOf('__sentinel__') > -1).toBe(true);
217
- React.unmountComponentAtNode(element);
218
- expect(element.innerHTML).toEqual('');
219
-
220
- // Now simulate a situation where the app is not idempotent. React should
221
- // warn but do the right thing.
222
- var _warn = console.warn;
223
- console.warn = mocks.getMockFunction();
224
- element.innerHTML = lastMarkup;
225
- var instance = React.renderComponent(<TestComponent name="y" />, element);
226
- expect(mountCount).toEqual(4);
227
- expect(console.warn.mock.calls.length).toBe(1);
228
- expect(element.innerHTML.length > 0).toBe(true);
229
- expect(element.innerHTML).not.toEqual(lastMarkup);
230
- console.warn = _warn;
231
-
232
- // Ensure the events system works
233
- expect(numClicks).toEqual(0);
234
- ReactTestUtils.Simulate.click(instance.refs.span.getDOMNode());
235
- expect(numClicks).toEqual(1);
236
- });
183
+ var element = document.createElement('div');
184
+ React.renderComponent(<TestComponent />, element);
185
+
186
+ var lastMarkup = element.innerHTML;
187
+
188
+ // Exercise the update path. Markup should not change,
189
+ // but some lifecycle methods should be run again.
190
+ React.renderComponent(<TestComponent name="x" />, element);
191
+ expect(mountCount).toEqual(1);
192
+
193
+ // Unmount and remount. We should get another mount event and
194
+ // we should get different markup, as the IDs are unique each time.
195
+ React.unmountComponentAtNode(element);
196
+ expect(element.innerHTML).toEqual('');
197
+ React.renderComponent(<TestComponent name="x" />, element);
198
+ expect(mountCount).toEqual(2);
199
+ expect(element.innerHTML).not.toEqual(lastMarkup);
200
+
201
+ // Now kill the node and render it on top of server-rendered markup, as if
202
+ // we used server rendering. We should mount again, but the markup should
203
+ // be unchanged. We will append a sentinel at the end of innerHTML to be
204
+ // sure that innerHTML was not changed.
205
+ React.unmountComponentAtNode(element);
206
+ expect(element.innerHTML).toEqual('');
207
+
208
+ ExecutionEnvironment.canUseDOM = false;
209
+ lastMarkup = ReactServerRendering.renderComponentToString(
210
+ <TestComponent name="x" />
211
+ );
212
+ ExecutionEnvironment.canUseDOM = true;
213
+ element.innerHTML = lastMarkup + ' __sentinel__';
214
+
215
+ React.renderComponent(<TestComponent name="x" />, element);
216
+ expect(mountCount).toEqual(3);
217
+ expect(element.innerHTML.indexOf('__sentinel__') > -1).toBe(true);
218
+ React.unmountComponentAtNode(element);
219
+ expect(element.innerHTML).toEqual('');
220
+
221
+ // Now simulate a situation where the app is not idempotent. React should
222
+ // warn but do the right thing.
223
+ var _warn = console.warn;
224
+ console.warn = mocks.getMockFunction();
225
+ element.innerHTML = lastMarkup;
226
+ var instance = React.renderComponent(<TestComponent name="y" />, element);
227
+ expect(mountCount).toEqual(4);
228
+ expect(console.warn.mock.calls.length).toBe(1);
229
+ expect(element.innerHTML.length > 0).toBe(true);
230
+ expect(element.innerHTML).not.toEqual(lastMarkup);
231
+ console.warn = _warn;
232
+
233
+ // Ensure the events system works
234
+ expect(numClicks).toEqual(0);
235
+ ReactTestUtils.Simulate.click(instance.refs.span.getDOMNode());
236
+ expect(numClicks).toEqual(1);
237
+ });
238
238
- it('should throw with silly args', function() {
239
- expect(
240
- ReactServerRendering.renderComponentToString.bind(
241
- ReactServerRendering,
242
- 'not a component'
243
- )
244
- ).toThrow(
245
- 'Invariant Violation: renderComponentToString(): You must pass ' +
246
- 'a valid ReactComponent.'
247
- );
248
- });
239
+ it('should throw with silly args', function() {
240
+ expect(
241
+ ReactServerRendering.renderComponentToString.bind(
242
+ ReactServerRendering,
243
+ 'not a component'
244
+ )
245
+ ).toThrow(
246
+ 'Invariant Violation: renderComponentToString(): You must pass ' +
247
+ 'a valid ReactComponent.'
248
+ );
249
+ });
250
250
- it('should provide guidance for breaking API changes', function() {
251
- expect(
252
- ReactServerRendering.renderComponentToString.bind(
253
- ReactServerRendering,
254
- <div />,
255
- function(){}
256
- )
257
- ).toThrow(
258
- 'Invariant Violation: renderComponentToString(): This function became ' +
259
- 'synchronous and now returns the generated markup. Please remove the ' +
260
- 'second parameter.'
261
- );
251
+ it('should provide guidance for breaking API changes', function() {
252
+ expect(
253
+ ReactServerRendering.renderComponentToString.bind(
254
+ ReactServerRendering,
255
+ <div />,
256
+ function(){}
257
+ )
258
+ ).toThrow(
259
+ 'Invariant Violation: renderComponentToString(): This function ' +
260
+ 'became synchronous and now returns the generated markup. Please ' +
261
+ 'remove the second parameter.'
262
+ );
263
+ });
264
});
265
264
- it('should not put checksum and React ID on components if specified so',
265
- function() {
266
+ describe('renderComponentToStaticMarkup', function() {
267
+ it('should not put checksum and React ID on components', function() {
268
var lifecycle = [];
269
var NestedComponent = React.createClass({
270
render: function() {
@@ -277,29 +279,99 @@ describe('ReactServerRendering', function() {
279
}
280
});
281
280
- var response = ReactServerRendering.renderComponentToString(
281
- <TestComponent />,
282
- {staticMarkup: true}
282
+ var response = ReactServerRendering.renderComponentToStaticMarkup(
283
+ <TestComponent />
284
);
285
286
expect(response).toBe('<span><div>inner text</div></span>');
286
- }
287
- );
287
+ });
288
289
- it('should not put checksum and React ID on text components if specified so',
290
- function() {
289
+ it('should not put checksum and React ID on text components', function() {
290
var TestComponent = React.createClass({
291
render: function() {
292
return <span>{'hello'} {'world'}</span>;
293
}
294
});
295
297
- var response = ReactServerRendering.renderComponentToString(
298
- <TestComponent />,
299
- {staticMarkup: true}
296
+ var response = ReactServerRendering.renderComponentToStaticMarkup(
297
+ <TestComponent />
298
);
299
300
expect(response).toBe('<span>hello world</span>');
303
- }
304
- );
301
+ });
302
+
303
+ it('should not register event listeners', function() {
304
+ var EventPluginHub = require('EventPluginHub');
305
+ var cb = mocks.getMockFunction();
306
+
307
+ ReactServerRendering.renderComponentToString(
308
+ <span onClick={cb}>hello world</span>
309
+ );
310
+ expect(EventPluginHub.__getListenerBank()).toEqual({});
311
+ });
312
+
313
+ it('should only execute certain lifecycle methods', function() {
314
+ function runTest() {
315
+ var lifecycle = [];
316
+ var TestComponent = React.createClass({
317
+ componentWillMount: function() {
318
+ lifecycle.push('componentWillMount');
319
+ },
320
+ componentDidMount: function() {
321
+ lifecycle.push('componentDidMount');
322
+ },
323
+ getInitialState: function() {
324
+ lifecycle.push('getInitialState');
325
+ return {name: 'TestComponent'};
326
+ },
327
+ render: function() {
328
+ lifecycle.push('render');
329
+ return <span>Component name: {this.state.name}</span>;
330
+ },
331
+ componentWillUpdate: function() {
332
+ lifecycle.push('componentWillUpdate');
333
+ },
334
+ componentDidUpdate: function() {
335
+ lifecycle.push('componentDidUpdate');
336
+ },
337
+ shouldComponentUpdate: function() {
338
+ lifecycle.push('shouldComponentUpdate');
339
+ },
340
+ componentWillReceiveProps: function() {
341
+ lifecycle.push('componentWillReceiveProps');
342
+ },
343
+ componentWillUnmount: function() {
344
+ lifecycle.push('componentWillUnmount');
345
+ }
346
+ });
347
+
348
+ var response = ReactServerRendering.renderComponentToStaticMarkup(
349
+ <TestComponent />
350
+ );
351
+
352
+ expect(response).toBe('<span>Component name: TestComponent</span>');
353
+ expect(lifecycle).toEqual(
354
+ ['getInitialState', 'componentWillMount', 'render']
355
+ );
356
+ }
357
+
358
+ runTest();
359
+
360
+ // This should work the same regardless of whether you can use DOM or not.
361
+ ExecutionEnvironment.canUseDOM = true;
362
+ runTest();
363
+ });
364
+
365
+ it('should throw with silly args', function() {
366
+ expect(
367
+ ReactServerRendering.renderComponentToStaticMarkup.bind(
368
+ ReactServerRendering,
369
+ 'not a component'
370
+ )
371
+ ).toThrow(
372
+ 'Invariant Violation: renderComponentToStaticMarkup(): You must pass ' +
373
+ 'a valid ReactComponent.'
374
+ );
375
+ });
376
+ });
377
});