@samitouri / QOS-React-2 / commits / 37cde3d864
Fork 0

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