@samitouri / QOS-React-2 / commits / 5545887a48

Allow rendering markup string without checksum and React ID

This fixes https://github.com/facebook/react/pull/994. Also fixes https://github.com/facebook/react/issues/1079. Server-rendering without ID/checksum now works. Also won't call didMount.

Cheng Lou committed Feb 20, 2014 at 15:31 UTC 5545887a48468ccc8b16c88713a8eb870ba209e6
9 files changed +270 -73
src/browser/ReactDOMComponent.js
+9 -5
@@ -99,7 +99,7 @@ ReactDOMComponent.Mixin = {
99 *
100 * @internal
101 * @param {string} rootID The root DOM ID for this node.
102 - * @param {ReactReconcileTransaction} transaction
102 + * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
103 * @param {number} mountDepth number of components in the owner hierarchy
104 * @return {string} The computed markup.
105 */
@@ -131,7 +131,7 @@ ReactDOMComponent.Mixin = {
131 * @see http://jsperf.com/obj-vs-arr-iteration
132 *
133 * @private
134 - * @param {ReactReconcileTransaction} transaction
134 + * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
135 * @return {string} Markup of opening tag.
136 */
137 _createOpenTagMarkupAndPutListeners: function(transaction) {
@@ -163,15 +163,19 @@ ReactDOMComponent.Mixin = {
163 }
164 }
165
166 - var idMarkup = DOMPropertyOperations.createMarkupForID(this._rootNodeID);
167 - return ret + ' ' + idMarkup + '>';
166 + if (!transaction.renderChecksumAndReactID) {
167 + return ret + '>';
168 + }
169 +
170 + var markupForID = DOMPropertyOperations.createMarkupForID(this._rootNodeID);
171 + return ret + ' ' + markupForID + '>';
172 },
173
174 /**
175 * Creates markup for the content between the tags.
176 *
177 * @private
174 - * @param {ReactReconcileTransaction} transaction
178 + * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
179 * @return {string} Content markup.
180 */
181 _createContentMarkup: function(transaction) {
src/browser/ReactReconcileTransaction.js
+7 -6
@@ -19,7 +19,6 @@
19
20 "use strict";
21
22 -var ExecutionEnvironment = require('ExecutionEnvironment');
22 var PooledClass = require('PooledClass');
23 var ReactEventEmitter = require('ReactEventEmitter');
24 var ReactInputSelection = require('ReactInputSelection');
@@ -127,6 +126,12 @@ var TRANSACTION_WRAPPERS = [
126 */
127 function ReactReconcileTransaction() {
128 this.reinitializeTransaction();
129 + // Only server-side rendering really needs this option (see
130 + // `ReactServerRendering`), but server-side uses
131 + // `ReactServerRenderingTransaction` instead. This option is here so that it's
132 + // accessible and defaults to true when `ReactDOMComponent` and
133 + // `ReactTextComponent` checks it in `mountComponent`.`
134 + this.renderChecksumAndReactID = true;
135 this.reactMountReady = ReactMountReady.getPooled(null);
136 this.putListenerQueue = ReactPutListenerQueue.getPooled();
137 }
@@ -140,11 +145,7 @@ var Mixin = {
145 * TODO: convert to array<TransactionWrapper>
146 */
147 getTransactionWrappers: function() {
143 - if (ExecutionEnvironment.canUseDOM) {
144 - return TRANSACTION_WRAPPERS;
145 - } else {
146 - return [];
147 - }
148 + return TRANSACTION_WRAPPERS;
149 },
150
151 /**
src/browser/ReactTextComponent.js
+12 -2
@@ -52,7 +52,7 @@ mixInto(ReactTextComponent, {
52 * any features besides containing text content.
53 *
54 * @param {string} rootID DOM ID of the root node.
55 - * @param {ReactReconcileTransaction} transaction
55 + * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
56 * @param {number} mountDepth number of components in the owner hierarchy
57 * @return {string} Markup for this text node.
58 * @internal
@@ -64,9 +64,19 @@ mixInto(ReactTextComponent, {
64 transaction,
65 mountDepth
66 );
67 +
68 + var escapedText = escapeTextForBrowser(this.props.text);
69 +
70 + if (!transaction.renderChecksumAndReactID) {
71 + // Normally we'd wrap this in a `span` for the reasons stated above, but
72 + // since this is a situation where React won't take over (static pages),
73 + // we can simply return the text as it is.
74 + return escapedText;
75 + }
76 +
77 return (
78 '<span ' + DOMPropertyOperations.createMarkupForID(rootID) + '>' +
69 - escapeTextForBrowser(this.props.text) +
79 + escapedText +
80 '</span>'
81 );
82 },
src/browser/server/ReactServerRendering.js
+24 -11
@@ -21,15 +21,17 @@
21 var ReactComponent = require('ReactComponent');
22 var ReactInstanceHandles = require('ReactInstanceHandles');
23 var ReactMarkupChecksum = require('ReactMarkupChecksum');
24 -var ReactReconcileTransaction = require('ReactReconcileTransaction');
24 +var ReactServerRenderingTransaction =
25 + require('ReactServerRenderingTransaction');
26 +
27
28 var invariant = require('invariant');
29
30 /**
31 * @param {ReactComponent} component
30 - * @return {string} the markup
32 + * @param {?object} options pass a boolean for the key `noChecksumNoID`
33 */
32 -function renderComponentToString(component) {
34 +function renderComponentToString(component, options) {
35 invariant(
36 ReactComponent.isValidComponent(component),
37 'renderComponentToString(): You must pass a valid ReactComponent.'
@@ -41,16 +43,27 @@ function renderComponentToString(component) {
43 'returns the generated markup. Please remove the second parameter.'
44 );
45
44 - var id = ReactInstanceHandles.createReactRootID();
45 - var transaction = ReactReconcileTransaction.getPooled();
46 - transaction.reinitializeTransaction();
46 + options = options || {};
47 +
48 + var transaction;
49 try {
48 - return transaction.perform(function() {
49 - var markup = component.mountComponent(id, transaction, 0);
50 - return ReactMarkupChecksum.addChecksumToMarkup(markup);
51 - }, null);
50 + if (options.noChecksumNoID) {
51 + transaction = ReactServerRenderingTransaction.getPooled(false);
52 +
53 + return transaction.perform(function() {
54 + return component.mountComponent(id, transaction, 0);
55 + }, null);
56 + } else {
57 + var id = ReactInstanceHandles.createReactRootID();
58 + transaction = ReactServerRenderingTransaction.getPooled(true);
59 +
60 + return transaction.perform(function() {
61 + var markup = component.mountComponent(id, transaction, 0);
62 + return ReactMarkupChecksum.addChecksumToMarkup(markup);
63 + }, null);
64 + }
65 } finally {
53 - ReactReconcileTransaction.release(transaction);
66 + ReactServerRenderingTransaction.release(transaction);
67 }
68 }
69
src/browser/server/__tests__/ReactServerRendering-test.js
+95 -45
@@ -101,53 +101,61 @@ describe('ReactServerRendering', function() {
101 });
102
103 it('should only execute certain lifecycle methods', function() {
104 - var lifecycle = [];
105 - var TestComponent = React.createClass({
106 - componentWillMount: function() {
107 - lifecycle.push('componentWillMount');
108 - },
109 - componentDidMount: function() {
110 - lifecycle.push('componentDidMount');
111 - },
112 - getInitialState: function() {
113 - lifecycle.push('getInitialState');
114 - return {name: 'TestComponent'};
115 - },
116 - render: function() {
117 - lifecycle.push('render');
118 - return <span>Component name: {this.state.name}</span>;
119 - },
120 - componentWillUpdate: function() {
121 - lifecycle.push('componentWillUpdate');
122 - },
123 - componentDidUpdate: function() {
124 - lifecycle.push('componentDidUpdate');
125 - },
126 - shouldComponentUpdate: function() {
127 - lifecycle.push('shouldComponentUpdate');
128 - },
129 - componentWillReceiveProps: function() {
130 - lifecycle.push('componentWillReceiveProps');
131 - },
132 - componentWillUnmount: function() {
133 - lifecycle.push('componentWillUnmount');
134 - }
135 - });
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 + },
117 + 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');
135 + }
136 + });
137
137 - var response = ReactServerRendering.renderComponentToString(
138 - <TestComponent />
139 - );
138 + var response = ReactServerRendering.renderComponentToString(
139 + <TestComponent />
140 + );
141
141 - expect(response).toMatch(
142 - '<span ' + ID_ATTRIBUTE_NAME + '="[^"]+" ' +
143 - ReactMarkupChecksum.CHECKSUM_ATTR_NAME + '="[^"]+">' +
144 - '<span ' + ID_ATTRIBUTE_NAME + '="[^"]+">Component name: </span>' +
145 - '<span ' + ID_ATTRIBUTE_NAME + '="[^"]+">TestComponent</span>' +
146 - '</span>'
147 - );
148 - expect(lifecycle).toEqual(
149 - ['getInitialState', 'componentWillMount', 'render']
150 - );
142 + expect(response).toMatch(
143 + '<span ' + ID_ATTRIBUTE_NAME + '="[^"]+" ' +
144 + ReactMarkupChecksum.CHECKSUM_ATTR_NAME + '="[^"]+">' +
145 + '<span ' + ID_ATTRIBUTE_NAME + '="[^"]+">Component name: </span>' +
146 + '<span ' + ID_ATTRIBUTE_NAME + '="[^"]+">TestComponent</span>' +
147 + '</span>'
148 + );
149 + expect(lifecycle).toEqual(
150 + ['getInitialState', 'componentWillMount', 'render']
151 + );
152 + }
153 +
154 + runTest();
155 +
156 + // This should work the same regardless of whether you can use DOM or not.
157 + ExecutionEnvironment.canUseDOM = true;
158 + runTest();
159 });
160
161 it('should have the correct mounting behavior', function() {
@@ -252,4 +260,46 @@ describe('ReactServerRendering', function() {
260 'second parameter.'
261 );
262 });
263 +
264 + it('should not put checksum and React ID on components if specified so',
265 + function() {
266 + var lifecycle = [];
267 + var NestedComponent = React.createClass({
268 + render: function() {
269 + return <div>inner text</div>;
270 + }
271 + });
272 +
273 + var TestComponent = React.createClass({
274 + render: function() {
275 + lifecycle.push('render');
276 + return <span><NestedComponent /></span>;
277 + }
278 + });
279 +
280 + var response = ReactServerRendering.renderComponentToString(
281 + <TestComponent />,
282 + {noChecksumNoID: true}
283 + );
284 +
285 + expect(response).toBe('<span><div>inner text</div></span>');
286 + }
287 + );
288 +
289 + it('should not put checksum and React ID on text components if specified so',
290 + function() {
291 + var TestComponent = React.createClass({
292 + render: function() {
293 + return <span>{'hello'} {'world'}</span>;
294 + }
295 + });
296 +
297 + var response = ReactServerRendering.renderComponentToString(
298 + <TestComponent />,
299 + {noChecksumNoID: true}
300 + );
301 +
302 + expect(response).toBe('<span>hello world</span>');
303 + }
304 + );
305 });
src/core/ReactComponent.js
+1 -2
@@ -354,7 +354,7 @@ var ReactComponent = {
354 * `ReactComponent.Mixin.mountComponent.call(this, ...)`.
355 *
356 * @param {string} rootID DOM ID of the root node.
357 - * @param {ReactReconcileTransaction} transaction
357 + * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
358 * @param {number} mountDepth number of components in the owner hierarchy.
359 * @return {?string} Rendered markup to be inserted into the DOM.
360 * @internal
@@ -425,7 +425,6 @@ var ReactComponent = {
425 /**
426 * Call `_performUpdateIfNecessary` within a new transaction.
427 *
428 - * @param {ReactReconcileTransaction} transaction
428 * @internal
429 */
430 performUpdateIfNecessary: function() {
src/core/ReactCompositeComponent.js
+1 -1
@@ -787,7 +787,7 @@ var ReactCompositeComponentMixin = {
787 * Initializes the component, renders markup, and registers event listeners.
788 *
789 * @param {string} rootID DOM ID of the root node.
790 - * @param {ReactReconcileTransaction} transaction
790 + * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
791 * @param {number} mountDepth number of components in the owner hierarchy
792 * @return {?string} Rendered markup to be inserted into the DOM.
793 * @final
src/core/ReactServerRenderingTransaction.js new
+116
@@ -0,0 +1,116 @@
1 +/**
2 + * Copyright 2014 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 ReactServerRenderingTransaction
17 + * @typechecks
18 + */
19 +
20 +"use strict";
21 +
22 +var PooledClass = require('PooledClass');
23 +var ReactMountReady = require('ReactMountReady');
24 +var ReactPutListenerQueue = require('ReactPutListenerQueue');
25 +var Transaction = require('Transaction');
26 +
27 +var emptyFunction = require('emptyFunction');
28 +var mixInto = require('mixInto');
29 +
30 +/**
31 + * Provides a `ReactMountReady` queue for collecting `onDOMReady` callbacks
32 + * during the performing of the transaction.
33 + */
34 +var ON_DOM_READY_QUEUEING = {
35 + /**
36 + * Initializes the internal `onDOMReady` queue.
37 + */
38 + initialize: function() {
39 + this.reactMountReady.reset();
40 + },
41 +
42 + close: emptyFunction
43 +};
44 +
45 +var PUT_LISTENER_QUEUEING = {
46 + initialize: function() {
47 + this.putListenerQueue.reset();
48 + },
49 +
50 + close: emptyFunction
51 +};
52 +
53 +/**
54 + * Executed within the scope of the `Transaction` instance. Consider these as
55 + * being member methods, but with an implied ordering while being isolated from
56 + * each other.
57 + */
58 +var TRANSACTION_WRAPPERS = [
59 + PUT_LISTENER_QUEUEING,
60 + ON_DOM_READY_QUEUEING
61 +];
62 +
63 +/**
64 + * @class ReactServerRenderingTransaction
65 + * @param {boolean} renderChecksumAndReactID
66 + */
67 +function ReactServerRenderingTransaction(renderChecksumAndReactID) {
68 + this.reinitializeTransaction();
69 + this.renderChecksumAndReactID = renderChecksumAndReactID;
70 + this.reactMountReady = ReactMountReady.getPooled(null);
71 + this.putListenerQueue = ReactPutListenerQueue.getPooled();
72 +}
73 +
74 +var Mixin = {
75 + /**
76 + * @see Transaction
77 + * @abstract
78 + * @final
79 + * @return {array} Empty list of operation wrap proceedures.
80 + */
81 + getTransactionWrappers: function() {
82 + return TRANSACTION_WRAPPERS;
83 + },
84 +
85 + /**
86 + * @return {object} The queue to collect `onDOMReady` callbacks with.
87 + * TODO: convert to ReactMountReady
88 + */
89 + getReactMountReady: function() {
90 + return this.reactMountReady;
91 + },
92 +
93 + getPutListenerQueue: function() {
94 + return this.putListenerQueue;
95 + },
96 +
97 + /**
98 + * `PooledClass` looks for this, and will invoke this before allowing this
99 + * instance to be resused.
100 + */
101 + destructor: function() {
102 + ReactMountReady.release(this.reactMountReady);
103 + this.reactMountReady = null;
104 +
105 + ReactPutListenerQueue.release(this.putListenerQueue);
106 + this.putListenerQueue = null;
107 + }
108 +};
109 +
110 +
111 +mixInto(ReactServerRenderingTransaction, Transaction.Mixin);
112 +mixInto(ReactServerRenderingTransaction, Mixin);
113 +
114 +PooledClass.addPoolingTo(ReactServerRenderingTransaction);
115 +
116 +module.exports = ReactServerRenderingTransaction;
src/core/__tests__/ReactDOMComponent-test.js
+5 -1
@@ -221,6 +221,7 @@ describe('ReactDOMComponent', function() {
221
222 var mixInto = require('mixInto');
223 var ReactDOMComponent = require('ReactDOMComponent');
224 + var ReactReconcileTransaction = require('ReactReconcileTransaction');
225
226 var NodeStub = function(initialProps) {
227 this.props = initialProps || {};
@@ -229,7 +230,10 @@ describe('ReactDOMComponent', function() {
230 mixInto(NodeStub, ReactDOMComponent.Mixin);
231
232 genMarkup = function(props) {
232 - return (new NodeStub(props))._createOpenTagMarkupAndPutListeners();
233 + var transaction = new ReactReconcileTransaction();
234 + return (new NodeStub(props))._createOpenTagMarkupAndPutListeners(
235 + transaction
236 + );
237 };
238
239 this.addMatchers({