The library for web and native user interfaces.
Stabilize minimal server rendering API
This is a pretty killer feature and the API is simple. I know it's another API method on React, but I think it's the only way.
CommitSyncScript committed
Jun 10, 2013 at 16:15 UTC
37cde3d864406f2af65dc03eae45a216ea969600
6 files changed
+340
-45
src/core/React.js
+2
@@ -22,6 +22,7 @@ var ReactCompositeComponent = require('ReactCompositeComponent');
22
var ReactComponent = require('ReactComponent');
23
var ReactDOM = require('ReactDOM');
24
var ReactMount = require('ReactMount');
25
+var ReactServerRendering = require('ReactServerRendering');
26
27
var ReactDefaultInjection = require('ReactDefaultInjection');
28
@@ -37,6 +38,7 @@ var React = {
38
constructAndRenderComponent: ReactMount.constructAndRenderComponent,
39
constructAndRenderComponentByID: ReactMount.constructAndRenderComponentByID,
40
renderComponent: ReactMount.renderComponent,
41
+ renderComponentToString: ReactServerRendering.renderComponentToString,
42
unmountAndReleaseReactRootNode: ReactMount.unmountAndReleaseReactRootNode,
43
isValidComponent: ReactComponent.isValidComponent
44
};
src/core/ReactComponent.js
+14
-3
@@ -401,18 +401,20 @@ var ReactComponent = {
401
*
402
* @param {string} rootID DOM ID of the root node.
403
* @param {DOMElement} container DOM element to mount into.
404
+ * @param {boolean} shouldReuseMarkup If true, do not insert markup
405
* @final
406
* @internal
407
* @see {ReactMount.renderComponent}
408
*/
408
- mountComponentIntoNode: function(rootID, container) {
409
+ mountComponentIntoNode: function(rootID, container, shouldReuseMarkup) {
410
var transaction = ReactComponent.ReactReconcileTransaction.getPooled();
411
transaction.perform(
412
this._mountComponentIntoNode,
413
this,
414
rootID,
415
container,
415
- transaction
416
+ transaction,
417
+ shouldReuseMarkup
418
);
419
ReactComponent.ReactReconcileTransaction.release(transaction);
420
},
@@ -421,14 +423,23 @@ var ReactComponent = {
423
* @param {string} rootID DOM ID of the root node.
424
* @param {DOMElement} container DOM element to mount into.
425
* @param {ReactReconcileTransaction} transaction
426
+ * @param {boolean} shouldReuseMarkup If true, do not insert markup
427
* @final
428
* @private
429
*/
427
- _mountComponentIntoNode: function(rootID, container, transaction) {
430
+ _mountComponentIntoNode: function(
431
+ rootID,
432
+ container,
433
+ transaction,
434
+ shouldReuseMarkup) {
435
var renderStart = Date.now();
436
var markup = this.mountComponent(rootID, transaction);
437
ReactMount.totalInstantiationTime += (Date.now() - renderStart);
438
439
+ if (shouldReuseMarkup) {
440
+ return;
441
+ }
442
+
443
var injectionStart = Date.now();
444
// Asynchronously inject markup by ensuring that the container is not in
445
// the document when settings its `innerHTML`.
src/core/ReactInstanceHandles.js
+39
-23
@@ -30,6 +30,26 @@ var SEPARATOR_LENGTH = SEPARATOR.length;
30
*/
31
var MAX_TREE_DEPTH = 100;
32
33
+/**
34
+ * Size of the reactRoot ID space. We generate random numbers for React root
35
+ * IDs and if there's a collision the events and DOM update system will
36
+ * get confused. If we assume 100 React components per page, and a user
37
+ * loads 1 page per minute 24/7 for 50 years, with a mount point space of
38
+ * 9,999,999 the likelihood of never having a collision is 99.997%.
39
+ */
40
+var GLOBAL_MOUNT_POINT_MAX = 9999999;
41
+
42
+/**
43
+ * Creates a DOM ID prefix to use when mounting React components.
44
+ *
45
+ * @param {number} index A unique integer
46
+ * @return {string} React root ID.
47
+ * @internal
48
+ */
49
+function getReactRootIDString(index) {
50
+ return '.reactRoot[' + index + ']';
51
+}
52
+
53
/**
54
* Checks if a character in the supplied ID is a separator or the end.
55
*
@@ -55,18 +75,6 @@ function isValidID(id) {
75
);
76
}
77
58
-/**
59
- * True if the supplied `node` is rendered by React.
60
- *
61
- * @param {DOMEventTarget} node DOM Element to check.
62
- * @return {boolean} True if the DOM Element appears to be rendered by React.
63
- * @private
64
- */
65
-function isRenderedByReact(node) {
66
- var id = getDOMNodeID(node);
67
- return id ? id.charAt(0) === SEPARATOR : false;
68
-}
69
-
78
/**
79
* Gets the parent ID of the supplied React DOM ID, `id`.
80
*
@@ -137,6 +145,24 @@ var ReactInstanceHandles = {
145
146
separator: SEPARATOR,
147
148
+ createReactRootID: function() {
149
+ return getReactRootIDString(
150
+ Math.ceil(Math.random() * GLOBAL_MOUNT_POINT_MAX)
151
+ );
152
+ },
153
+
154
+ /**
155
+ * True if the supplied `node` is rendered by React.
156
+ *
157
+ * @param {DOMEventTarget} node DOM Element to check.
158
+ * @return {boolean} True if the DOM Element appears to be rendered by React.
159
+ * @private
160
+ */
161
+ isRenderedByReact: function(node) {
162
+ var id = getDOMNodeID(node);
163
+ return id ? id.charAt(0) === SEPARATOR : false;
164
+ },
165
+
166
/**
167
* Traverses up the ancestors of the supplied node to find a node that is a
168
* DOM representation of a React component.
@@ -148,7 +174,7 @@ var ReactInstanceHandles = {
174
getFirstReactDOM: function(node) {
175
var current = node;
176
while (current && current.parentNode !== current) {
151
- if (isRenderedByReact(current)) {
177
+ if (ReactInstanceHandles.isRenderedByReact(current)) {
178
return current;
179
}
180
current = current.parentNode;
@@ -213,16 +239,6 @@ var ReactInstanceHandles = {
239
);
240
return longestCommonID;
241
},
216
- /**
217
- * Creates a DOM ID to use when mounting React components.
218
- *
219
- * @param {number} mountPointCount The count of React renders so far.
220
- * @return {string} React root ID.
221
- * @internal
222
- */
223
- getReactRootID: function(mountPointCount) {
224
- return '.reactRoot[' + mountPointCount + ']';
225
- },
242
243
/**
244
* Gets the DOM ID of the React component that is the root of the tree that
src/core/ReactMount.js
+79
-19
@@ -24,20 +24,27 @@ var ReactEventTopLevelCallback = require('ReactEventTopLevelCallback');
24
25
var $ = require('$');
26
27
-var globalMountPointCounter = 0;
28
-
27
/** Mapping from reactRoot DOM ID to React component instance. */
28
var instanceByReactRootID = {};
29
30
/** Mapping from reactRoot DOM ID to `container` nodes. */
31
var containersByReactRootID = {};
32
33
+/**
34
+ * @param {DOMElement} container DOM element that may contain a React component
35
+ * @return {?DOMElement} DOM element that may have the reactRoot ID, or null.
36
+ */
37
+function getReactRootElementInContainer(container) {
38
+ return container.firstChild;
39
+}
40
+
41
/**
42
* @param {DOMElement} container DOM element that may contain a React component.
43
* @return {?string} A "reactRoot" ID, if a React component is rendered.
44
*/
45
function getReactRootID(container) {
40
- return container.firstChild && container.firstChild.id;
46
+ var rootElement = getReactRootElementInContainer(container);
47
+ return rootElement && rootElement.id;
48
}
49
50
/**
@@ -92,6 +99,54 @@ var ReactMount = {
99
);
100
},
101
102
+ /**
103
+ * Take a component that's already mounted into the DOM and replace its props
104
+ * @param {ReactComponent} prevComponent component instance already in the DOM
105
+ * @param {ReactComponent} nextComponent component instance to render
106
+ * @param {DOMElement} container container to render into
107
+ */
108
+ _updateRootComponent: function(prevComponent, nextComponent, container) {
109
+ var nextProps = nextComponent.props;
110
+ ReactMount.scrollMonitor(container, function() {
111
+ prevComponent.replaceProps(nextProps);
112
+ });
113
+ return prevComponent;
114
+ },
115
+
116
+ /**
117
+ * Register a component into the instance map and start the events system.
118
+ * @param {ReactComponent} nextComponent component instance to render
119
+ * @param {DOMElement} container container to render into
120
+ * @return {string} reactRoot ID prefix
121
+ */
122
+ _registerComponent: function(nextComponent, container) {
123
+ ReactMount.prepareTopLevelEvents(ReactEventTopLevelCallback);
124
+
125
+ var reactRootID = ReactMount.registerContainer(container);
126
+ instanceByReactRootID[reactRootID] = nextComponent;
127
+ return reactRootID;
128
+ },
129
+
130
+ /**
131
+ * Render a new component into the DOM.
132
+ * @param {ReactComponent} nextComponent component instance to render
133
+ * @param {DOMElement} container container to render into
134
+ * @param {boolean} shouldReuseMarkup if we should skip the markup insertion
135
+ * @return {ReactComponent} nextComponent
136
+ */
137
+ _renderNewRootComponent: function(
138
+ nextComponent,
139
+ container,
140
+ shouldReuseMarkup) {
141
+ var reactRootID = ReactMount._registerComponent(nextComponent, container);
142
+ nextComponent.mountComponentIntoNode(
143
+ reactRootID,
144
+ container,
145
+ shouldReuseMarkup
146
+ );
147
+ return nextComponent;
148
+ },
149
+
150
/**
151
* Renders a React component into the DOM in the supplied `container`.
152
*
@@ -104,25 +159,32 @@ var ReactMount = {
159
* @return {ReactComponent} Component instance rendered in `container`.
160
*/
161
renderComponent: function(nextComponent, container) {
107
- var prevComponent = instanceByReactRootID[getReactRootID(container)];
108
- if (prevComponent) {
109
- if (prevComponent.constructor === nextComponent.constructor) {
110
- var nextProps = nextComponent.props;
111
- ReactMount.scrollMonitor(container, function() {
112
- prevComponent.replaceProps(nextProps);
113
- });
114
- return prevComponent;
162
+ var registeredComponent = instanceByReactRootID[getReactRootID(container)];
163
+
164
+ if (registeredComponent) {
165
+ if (registeredComponent.constructor === nextComponent.constructor) {
166
+ return ReactMount._updateRootComponent(
167
+ registeredComponent,
168
+ nextComponent,
169
+ container
170
+ );
171
} else {
172
ReactMount.unmountAndReleaseReactRootNode(container);
173
}
174
}
175
120
- ReactMount.prepareTopLevelEvents(ReactEventTopLevelCallback);
176
+ var reactRootElement = getReactRootElementInContainer(container);
177
+ var containerHasReactMarkup =
178
+ reactRootElement &&
179
+ ReactInstanceHandles.isRenderedByReact(reactRootElement);
180
122
- var reactRootID = ReactMount.registerContainer(container);
123
- instanceByReactRootID[reactRootID] = nextComponent;
124
- nextComponent.mountComponentIntoNode(reactRootID, container);
125
- return nextComponent;
181
+ var shouldReuseMarkup = containerHasReactMarkup && !registeredComponent;
182
+
183
+ return ReactMount._renderNewRootComponent(
184
+ nextComponent,
185
+ container,
186
+ shouldReuseMarkup
187
+ );
188
},
189
190
/**
@@ -167,9 +229,7 @@ var ReactMount = {
229
}
230
if (!reactRootID) {
231
// No valid "reactRoot" ID found, create one.
170
- reactRootID = ReactInstanceHandles.getReactRootID(
171
- globalMountPointCounter++
172
- );
232
+ reactRootID = ReactInstanceHandles.createReactRootID();
233
}
234
containersByReactRootID[reactRootID] = container;
235
return reactRootID;
src/environment/ReactServerRendering.js
new
+31
@@ -0,0 +1,31 @@
1
+/**
2
+ * @typechecks
3
+ * @providesModule ReactServerRendering
4
+ */
5
+"use strict";
6
+
7
+var ReactReconcileTransaction = require('ReactReconcileTransaction');
8
+var ReactInstanceHandles = require('ReactInstanceHandles');
9
+
10
+/**
11
+ * @param {object} component
12
+ * @param {function} callback
13
+ */
14
+function renderComponentToString(component, callback) {
15
+ // We use a callback API to keep the API async in case in the future we ever
16
+ // need it, but in reality this is a synchronous operation.
17
+ var id = ReactInstanceHandles.createReactRootID();
18
+ var transaction = ReactReconcileTransaction.getPooled();
19
+ transaction.reinitializeTransaction();
20
+ try {
21
+ transaction.perform(function() {
22
+ callback(component.mountComponent(id, transaction));
23
+ }, null);
24
+ } finally {
25
+ ReactReconcileTransaction.release(transaction);
26
+ }
27
+}
28
+
29
+module.exports = {
30
+ renderComponentToString: renderComponentToString
31
+};
src/environment/__tests__/ReactServerRendering-test.js
new
+175
@@ -0,0 +1,175 @@
1
+/**
2
+ * @jsx React.DOM
3
+ * @emails react-core
4
+ */
5
+
6
+"use strict";
7
+
8
+require('mock-modules')
9
+ .dontMock('ExecutionEnvironment')
10
+ .dontMock('React')
11
+ .dontMock('ReactServerRendering')
12
+ .dontMock('ReactTestUtils');
13
+
14
+var React;
15
+var ReactTestUtils;
16
+var ReactServerRendering;
17
+var ExecutionEnvironment;
18
+
19
+describe('ReactServerRendering', function() {
20
+ beforeEach(function() {
21
+ require('mock-modules').dumpCache();
22
+ React = require('React');
23
+ ReactTestUtils = require('ReactTestUtils');
24
+ ExecutionEnvironment = require('ExecutionEnvironment');
25
+ ExecutionEnvironment.canUseDOM = false;
26
+ ReactServerRendering = require('ReactServerRendering');
27
+ });
28
+
29
+ it('should generate simple markup', function() {
30
+ var response;
31
+ ReactServerRendering.renderComponentToString(
32
+ <span>hello world</span>,
33
+ function(response_) {
34
+ response = response_;
35
+ }
36
+ );
37
+ expect(response).toMatch(
38
+ '<span id="[^"]+">hello world</span>'
39
+ );
40
+ });
41
+
42
+ it('should render composite components', function() {
43
+ var Parent = React.createClass({
44
+ render: function() {
45
+ return <div><Child name="child" /></div>;
46
+ }
47
+ });
48
+ var Child = React.createClass({
49
+ render: function() {
50
+ return <span>My name is {this.props.name}</span>;
51
+ }
52
+ });
53
+ var response;
54
+ ReactServerRendering.renderComponentToString(
55
+ <Parent />,
56
+ function(response_) {
57
+ response = response_;
58
+ }
59
+ );
60
+ expect(response).toMatch(
61
+ '<div id="[^"]+"><span id="[^"]+"><span id="[^"]+">My name is </span>' +
62
+ '<span id="[^"]+">child</span></span></div>'
63
+ );
64
+ });
65
+
66
+ it('should only execute certain lifecycle methods', function() {
67
+ var lifecycle = [];
68
+ var TestComponent = React.createClass({
69
+ componentWillMount: function() {
70
+ lifecycle.push('componentWillMount');
71
+ },
72
+ componentDidMount: function() {
73
+ lifecycle.push('componentDidMount');
74
+ },
75
+ getInitialState: function() {
76
+ lifecycle.push('getInitialState');
77
+ return {name: 'TestComponent'};
78
+ },
79
+ render: function() {
80
+ lifecycle.push('render');
81
+ return <span>Component name: {this.state.name}</span>;
82
+ },
83
+ componentWillUpdate: function() {
84
+ lifecycle.push('componentWillUpdate');
85
+ },
86
+ componentDidUpdate: function() {
87
+ lifecycle.push('componentDidUpdate');
88
+ },
89
+ shouldComponentUpdate: function() {
90
+ lifecycle.push('shouldComponentUpdate');
91
+ },
92
+ componentWillReceiveProps: function() {
93
+ lifecycle.push('componentWillReceiveProps');
94
+ },
95
+ componentWillUnmount: function() {
96
+ lifecycle.push('componentWillUnmount');
97
+ }
98
+ });
99
+
100
+ var response;
101
+
102
+ ReactServerRendering.renderComponentToString(
103
+ <TestComponent />,
104
+ function (_response) {
105
+ response = _response;
106
+ }
107
+ );
108
+
109
+ expect(response).toMatch(
110
+ '<span id="[^"]+"><span id="[^"]+">Component name: </span>' +
111
+ '<span id="[^"]+">TestComponent</span></span>'
112
+ );
113
+ expect(lifecycle).toEqual(
114
+ ['getInitialState', 'componentWillMount', 'render']
115
+ );
116
+ });
117
+
118
+ it('should have the correct mounting behavior', function() {
119
+ // This test is testing client-side behavior.
120
+ ExecutionEnvironment.canUseDOM = true;
121
+
122
+ var mountCount = 0;
123
+ var numClicks = 0;
124
+
125
+ var TestComponent = React.createClass({
126
+ componentWillMount: function() {
127
+ mountCount++;
128
+ },
129
+ click: function() {
130
+ numClicks++;
131
+ },
132
+ render: function() {
133
+ return (
134
+ <span ref="span" onClick={this.click}>Name: {this.props.name}</span>
135
+ );
136
+ },
137
+ });
138
+
139
+ var element = document.createElement('div');
140
+ React.renderComponent(<TestComponent />, element);
141
+
142
+ var lastMarkup = element.innerHTML;
143
+
144
+ // Exercise the update path. Markup should not change,
145
+ // but some lifecycle methods should be run again.
146
+ React.renderComponent(<TestComponent name="x" />, element);
147
+ expect(mountCount).toEqual(1);
148
+
149
+ // Unmount and remount. We should get another mount event and
150
+ // we should get different markup, as the IDs are unique each time.
151
+ React.unmountAndReleaseReactRootNode(element);
152
+ expect(element.innerHTML).toEqual('');
153
+ React.renderComponent(<TestComponent name="x" />, element);
154
+ expect(mountCount).toEqual(2);
155
+ expect(element.innerHTML).not.toEqual(lastMarkup);
156
+
157
+ // Now kill the node and render it on top of the old markup, as if
158
+ // we used server rendering. We should mount again, but the markup should be
159
+ // unchanged.
160
+ lastMarkup = element.innerHTML;
161
+ React.unmountAndReleaseReactRootNode(element);
162
+ expect(element.innerHTML).toEqual('');
163
+ element.innerHTML = lastMarkup;
164
+ // NOTE: we pass a different name here. This is to ensure that the markup
165
+ // being generated is not replaced.
166
+ var instance = React.renderComponent(<TestComponent name="y" />, element);
167
+ expect(mountCount).toEqual(3);
168
+ expect(element.innerHTML).toEqual(lastMarkup);
169
+
170
+ // Ensure the events system works
171
+ expect(numClicks).toEqual(0);
172
+ ReactTestUtils.Simulate.click(instance.refs.span.getDOMNode());
173
+ expect(numClicks).toEqual(1);
174
+ });
175
+});