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({