@samitouri / QOS-React-2 / commits / 99dab49f92

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 });