mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
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.
This commit is contained in:
1 parent
7e7579e1ba
commit
37cde3d864
6 files changed
+340
-45
No files matched your search
@@ -22,6 +22,7 @@ var ReactCompositeComponent = require('ReactCompositeComponent');
|
||||
var ReactComponent = require('ReactComponent');
|
||||
var ReactDOM = require('ReactDOM');
|
||||
var ReactMount = require('ReactMount');
|
||||
var ReactServerRendering = require('ReactServerRendering');
|
||||
|
||||
var ReactDefaultInjection = require('ReactDefaultInjection');
|
||||
|
||||
@@ -37,6 +38,7 @@ var React = {
|
||||
constructAndRenderComponent: ReactMount.constructAndRenderComponent,
|
||||
constructAndRenderComponentByID: ReactMount.constructAndRenderComponentByID,
|
||||
renderComponent: ReactMount.renderComponent,
|
||||
renderComponentToString: ReactServerRendering.renderComponentToString,
|
||||
unmountAndReleaseReactRootNode: ReactMount.unmountAndReleaseReactRootNode,
|
||||
isValidComponent: ReactComponent.isValidComponent
|
||||
};
|
||||
|
||||
@@ -401,18 +401,20 @@ var ReactComponent = {
|
||||
*
|
||||
* @param {string} rootID DOM ID of the root node.
|
||||
* @param {DOMElement} container DOM element to mount into.
|
||||
* @param {boolean} shouldReuseMarkup If true, do not insert markup
|
||||
* @final
|
||||
* @internal
|
||||
* @see {ReactMount.renderComponent}
|
||||
*/
|
||||
mountComponentIntoNode: function(rootID, container) {
|
||||
mountComponentIntoNode: function(rootID, container, shouldReuseMarkup) {
|
||||
var transaction = ReactComponent.ReactReconcileTransaction.getPooled();
|
||||
transaction.perform(
|
||||
this._mountComponentIntoNode,
|
||||
this,
|
||||
rootID,
|
||||
container,
|
||||
transaction
|
||||
transaction,
|
||||
shouldReuseMarkup
|
||||
);
|
||||
ReactComponent.ReactReconcileTransaction.release(transaction);
|
||||
},
|
||||
@@ -421,14 +423,23 @@ var ReactComponent = {
|
||||
* @param {string} rootID DOM ID of the root node.
|
||||
* @param {DOMElement} container DOM element to mount into.
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
* @param {boolean} shouldReuseMarkup If true, do not insert markup
|
||||
* @final
|
||||
* @private
|
||||
*/
|
||||
_mountComponentIntoNode: function(rootID, container, transaction) {
|
||||
_mountComponentIntoNode: function(
|
||||
rootID,
|
||||
container,
|
||||
transaction,
|
||||
shouldReuseMarkup) {
|
||||
var renderStart = Date.now();
|
||||
var markup = this.mountComponent(rootID, transaction);
|
||||
ReactMount.totalInstantiationTime += (Date.now() - renderStart);
|
||||
|
||||
if (shouldReuseMarkup) {
|
||||
return;
|
||||
}
|
||||
|
||||
var injectionStart = Date.now();
|
||||
// Asynchronously inject markup by ensuring that the container is not in
|
||||
// the document when settings its `innerHTML`.
|
||||
|
||||
@@ -30,6 +30,26 @@ var SEPARATOR_LENGTH = SEPARATOR.length;
|
||||
*/
|
||||
var MAX_TREE_DEPTH = 100;
|
||||
|
||||
/**
|
||||
* Size of the reactRoot ID space. We generate random numbers for React root
|
||||
* IDs and if there's a collision the events and DOM update system will
|
||||
* get confused. If we assume 100 React components per page, and a user
|
||||
* loads 1 page per minute 24/7 for 50 years, with a mount point space of
|
||||
* 9,999,999 the likelihood of never having a collision is 99.997%.
|
||||
*/
|
||||
var GLOBAL_MOUNT_POINT_MAX = 9999999;
|
||||
|
||||
/**
|
||||
* Creates a DOM ID prefix to use when mounting React components.
|
||||
*
|
||||
* @param {number} index A unique integer
|
||||
* @return {string} React root ID.
|
||||
* @internal
|
||||
*/
|
||||
function getReactRootIDString(index) {
|
||||
return '.reactRoot[' + index + ']';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a character in the supplied ID is a separator or the end.
|
||||
*
|
||||
@@ -55,18 +75,6 @@ function isValidID(id) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the supplied `node` is rendered by React.
|
||||
*
|
||||
* @param {DOMEventTarget} node DOM Element to check.
|
||||
* @return {boolean} True if the DOM Element appears to be rendered by React.
|
||||
* @private
|
||||
*/
|
||||
function isRenderedByReact(node) {
|
||||
var id = getDOMNodeID(node);
|
||||
return id ? id.charAt(0) === SEPARATOR : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the parent ID of the supplied React DOM ID, `id`.
|
||||
*
|
||||
@@ -137,6 +145,24 @@ var ReactInstanceHandles = {
|
||||
|
||||
separator: SEPARATOR,
|
||||
|
||||
createReactRootID: function() {
|
||||
return getReactRootIDString(
|
||||
Math.ceil(Math.random() * GLOBAL_MOUNT_POINT_MAX)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* True if the supplied `node` is rendered by React.
|
||||
*
|
||||
* @param {DOMEventTarget} node DOM Element to check.
|
||||
* @return {boolean} True if the DOM Element appears to be rendered by React.
|
||||
* @private
|
||||
*/
|
||||
isRenderedByReact: function(node) {
|
||||
var id = getDOMNodeID(node);
|
||||
return id ? id.charAt(0) === SEPARATOR : false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Traverses up the ancestors of the supplied node to find a node that is a
|
||||
* DOM representation of a React component.
|
||||
@@ -148,7 +174,7 @@ var ReactInstanceHandles = {
|
||||
getFirstReactDOM: function(node) {
|
||||
var current = node;
|
||||
while (current && current.parentNode !== current) {
|
||||
if (isRenderedByReact(current)) {
|
||||
if (ReactInstanceHandles.isRenderedByReact(current)) {
|
||||
return current;
|
||||
}
|
||||
current = current.parentNode;
|
||||
@@ -213,16 +239,6 @@ var ReactInstanceHandles = {
|
||||
);
|
||||
return longestCommonID;
|
||||
},
|
||||
/**
|
||||
* Creates a DOM ID to use when mounting React components.
|
||||
*
|
||||
* @param {number} mountPointCount The count of React renders so far.
|
||||
* @return {string} React root ID.
|
||||
* @internal
|
||||
*/
|
||||
getReactRootID: function(mountPointCount) {
|
||||
return '.reactRoot[' + mountPointCount + ']';
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the DOM ID of the React component that is the root of the tree that
|
||||
|
||||
+79
-19
@@ -24,20 +24,27 @@ var ReactEventTopLevelCallback = require('ReactEventTopLevelCallback');
|
||||
|
||||
var $ = require('$');
|
||||
|
||||
var globalMountPointCounter = 0;
|
||||
|
||||
/** Mapping from reactRoot DOM ID to React component instance. */
|
||||
var instanceByReactRootID = {};
|
||||
|
||||
/** Mapping from reactRoot DOM ID to `container` nodes. */
|
||||
var containersByReactRootID = {};
|
||||
|
||||
/**
|
||||
* @param {DOMElement} container DOM element that may contain a React component
|
||||
* @return {?DOMElement} DOM element that may have the reactRoot ID, or null.
|
||||
*/
|
||||
function getReactRootElementInContainer(container) {
|
||||
return container.firstChild;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DOMElement} container DOM element that may contain a React component.
|
||||
* @return {?string} A "reactRoot" ID, if a React component is rendered.
|
||||
*/
|
||||
function getReactRootID(container) {
|
||||
return container.firstChild && container.firstChild.id;
|
||||
var rootElement = getReactRootElementInContainer(container);
|
||||
return rootElement && rootElement.id;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,6 +99,54 @@ var ReactMount = {
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Take a component that's already mounted into the DOM and replace its props
|
||||
* @param {ReactComponent} prevComponent component instance already in the DOM
|
||||
* @param {ReactComponent} nextComponent component instance to render
|
||||
* @param {DOMElement} container container to render into
|
||||
*/
|
||||
_updateRootComponent: function(prevComponent, nextComponent, container) {
|
||||
var nextProps = nextComponent.props;
|
||||
ReactMount.scrollMonitor(container, function() {
|
||||
prevComponent.replaceProps(nextProps);
|
||||
});
|
||||
return prevComponent;
|
||||
},
|
||||
|
||||
/**
|
||||
* Register a component into the instance map and start the events system.
|
||||
* @param {ReactComponent} nextComponent component instance to render
|
||||
* @param {DOMElement} container container to render into
|
||||
* @return {string} reactRoot ID prefix
|
||||
*/
|
||||
_registerComponent: function(nextComponent, container) {
|
||||
ReactMount.prepareTopLevelEvents(ReactEventTopLevelCallback);
|
||||
|
||||
var reactRootID = ReactMount.registerContainer(container);
|
||||
instanceByReactRootID[reactRootID] = nextComponent;
|
||||
return reactRootID;
|
||||
},
|
||||
|
||||
/**
|
||||
* Render a new component into the DOM.
|
||||
* @param {ReactComponent} nextComponent component instance to render
|
||||
* @param {DOMElement} container container to render into
|
||||
* @param {boolean} shouldReuseMarkup if we should skip the markup insertion
|
||||
* @return {ReactComponent} nextComponent
|
||||
*/
|
||||
_renderNewRootComponent: function(
|
||||
nextComponent,
|
||||
container,
|
||||
shouldReuseMarkup) {
|
||||
var reactRootID = ReactMount._registerComponent(nextComponent, container);
|
||||
nextComponent.mountComponentIntoNode(
|
||||
reactRootID,
|
||||
container,
|
||||
shouldReuseMarkup
|
||||
);
|
||||
return nextComponent;
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders a React component into the DOM in the supplied `container`.
|
||||
*
|
||||
@@ -104,25 +159,32 @@ var ReactMount = {
|
||||
* @return {ReactComponent} Component instance rendered in `container`.
|
||||
*/
|
||||
renderComponent: function(nextComponent, container) {
|
||||
var prevComponent = instanceByReactRootID[getReactRootID(container)];
|
||||
if (prevComponent) {
|
||||
if (prevComponent.constructor === nextComponent.constructor) {
|
||||
var nextProps = nextComponent.props;
|
||||
ReactMount.scrollMonitor(container, function() {
|
||||
prevComponent.replaceProps(nextProps);
|
||||
});
|
||||
return prevComponent;
|
||||
var registeredComponent = instanceByReactRootID[getReactRootID(container)];
|
||||
|
||||
if (registeredComponent) {
|
||||
if (registeredComponent.constructor === nextComponent.constructor) {
|
||||
return ReactMount._updateRootComponent(
|
||||
registeredComponent,
|
||||
nextComponent,
|
||||
container
|
||||
);
|
||||
} else {
|
||||
ReactMount.unmountAndReleaseReactRootNode(container);
|
||||
}
|
||||
}
|
||||
|
||||
ReactMount.prepareTopLevelEvents(ReactEventTopLevelCallback);
|
||||
var reactRootElement = getReactRootElementInContainer(container);
|
||||
var containerHasReactMarkup =
|
||||
reactRootElement &&
|
||||
ReactInstanceHandles.isRenderedByReact(reactRootElement);
|
||||
|
||||
var reactRootID = ReactMount.registerContainer(container);
|
||||
instanceByReactRootID[reactRootID] = nextComponent;
|
||||
nextComponent.mountComponentIntoNode(reactRootID, container);
|
||||
return nextComponent;
|
||||
var shouldReuseMarkup = containerHasReactMarkup && !registeredComponent;
|
||||
|
||||
return ReactMount._renderNewRootComponent(
|
||||
nextComponent,
|
||||
container,
|
||||
shouldReuseMarkup
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -167,9 +229,7 @@ var ReactMount = {
|
||||
}
|
||||
if (!reactRootID) {
|
||||
// No valid "reactRoot" ID found, create one.
|
||||
reactRootID = ReactInstanceHandles.getReactRootID(
|
||||
globalMountPointCounter++
|
||||
);
|
||||
reactRootID = ReactInstanceHandles.createReactRootID();
|
||||
}
|
||||
containersByReactRootID[reactRootID] = container;
|
||||
return reactRootID;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* @typechecks
|
||||
* @providesModule ReactServerRendering
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
var ReactReconcileTransaction = require('ReactReconcileTransaction');
|
||||
var ReactInstanceHandles = require('ReactInstanceHandles');
|
||||
|
||||
/**
|
||||
* @param {object} component
|
||||
* @param {function} callback
|
||||
*/
|
||||
function renderComponentToString(component, callback) {
|
||||
// We use a callback API to keep the API async in case in the future we ever
|
||||
// need it, but in reality this is a synchronous operation.
|
||||
var id = ReactInstanceHandles.createReactRootID();
|
||||
var transaction = ReactReconcileTransaction.getPooled();
|
||||
transaction.reinitializeTransaction();
|
||||
try {
|
||||
transaction.perform(function() {
|
||||
callback(component.mountComponent(id, transaction));
|
||||
}, null);
|
||||
} finally {
|
||||
ReactReconcileTransaction.release(transaction);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
renderComponentToString: renderComponentToString
|
||||
};
|
||||
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* @jsx React.DOM
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
require('mock-modules')
|
||||
.dontMock('ExecutionEnvironment')
|
||||
.dontMock('React')
|
||||
.dontMock('ReactServerRendering')
|
||||
.dontMock('ReactTestUtils');
|
||||
|
||||
var React;
|
||||
var ReactTestUtils;
|
||||
var ReactServerRendering;
|
||||
var ExecutionEnvironment;
|
||||
|
||||
describe('ReactServerRendering', function() {
|
||||
beforeEach(function() {
|
||||
require('mock-modules').dumpCache();
|
||||
React = require('React');
|
||||
ReactTestUtils = require('ReactTestUtils');
|
||||
ExecutionEnvironment = require('ExecutionEnvironment');
|
||||
ExecutionEnvironment.canUseDOM = false;
|
||||
ReactServerRendering = require('ReactServerRendering');
|
||||
});
|
||||
|
||||
it('should generate simple markup', function() {
|
||||
var response;
|
||||
ReactServerRendering.renderComponentToString(
|
||||
<span>hello world</span>,
|
||||
function(response_) {
|
||||
response = response_;
|
||||
}
|
||||
);
|
||||
expect(response).toMatch(
|
||||
'<span id="[^"]+">hello world</span>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should render composite components', function() {
|
||||
var Parent = React.createClass({
|
||||
render: function() {
|
||||
return <div><Child name="child" /></div>;
|
||||
}
|
||||
});
|
||||
var Child = React.createClass({
|
||||
render: function() {
|
||||
return <span>My name is {this.props.name}</span>;
|
||||
}
|
||||
});
|
||||
var response;
|
||||
ReactServerRendering.renderComponentToString(
|
||||
<Parent />,
|
||||
function(response_) {
|
||||
response = response_;
|
||||
}
|
||||
);
|
||||
expect(response).toMatch(
|
||||
'<div id="[^"]+"><span id="[^"]+"><span id="[^"]+">My name is </span>' +
|
||||
'<span id="[^"]+">child</span></span></div>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should only execute certain lifecycle methods', function() {
|
||||
var lifecycle = [];
|
||||
var TestComponent = React.createClass({
|
||||
componentWillMount: function() {
|
||||
lifecycle.push('componentWillMount');
|
||||
},
|
||||
componentDidMount: function() {
|
||||
lifecycle.push('componentDidMount');
|
||||
},
|
||||
getInitialState: function() {
|
||||
lifecycle.push('getInitialState');
|
||||
return {name: 'TestComponent'};
|
||||
},
|
||||
render: function() {
|
||||
lifecycle.push('render');
|
||||
return <span>Component name: {this.state.name}</span>;
|
||||
},
|
||||
componentWillUpdate: function() {
|
||||
lifecycle.push('componentWillUpdate');
|
||||
},
|
||||
componentDidUpdate: function() {
|
||||
lifecycle.push('componentDidUpdate');
|
||||
},
|
||||
shouldComponentUpdate: function() {
|
||||
lifecycle.push('shouldComponentUpdate');
|
||||
},
|
||||
componentWillReceiveProps: function() {
|
||||
lifecycle.push('componentWillReceiveProps');
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
lifecycle.push('componentWillUnmount');
|
||||
}
|
||||
});
|
||||
|
||||
var response;
|
||||
|
||||
ReactServerRendering.renderComponentToString(
|
||||
<TestComponent />,
|
||||
function (_response) {
|
||||
response = _response;
|
||||
}
|
||||
);
|
||||
|
||||
expect(response).toMatch(
|
||||
'<span id="[^"]+"><span id="[^"]+">Component name: </span>' +
|
||||
'<span id="[^"]+">TestComponent</span></span>'
|
||||
);
|
||||
expect(lifecycle).toEqual(
|
||||
['getInitialState', 'componentWillMount', 'render']
|
||||
);
|
||||
});
|
||||
|
||||
it('should have the correct mounting behavior', function() {
|
||||
// This test is testing client-side behavior.
|
||||
ExecutionEnvironment.canUseDOM = true;
|
||||
|
||||
var mountCount = 0;
|
||||
var numClicks = 0;
|
||||
|
||||
var TestComponent = React.createClass({
|
||||
componentWillMount: function() {
|
||||
mountCount++;
|
||||
},
|
||||
click: function() {
|
||||
numClicks++;
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
<span ref="span" onClick={this.click}>Name: {this.props.name}</span>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
var element = document.createElement('div');
|
||||
React.renderComponent(<TestComponent />, element);
|
||||
|
||||
var lastMarkup = element.innerHTML;
|
||||
|
||||
// Exercise the update path. Markup should not change,
|
||||
// but some lifecycle methods should be run again.
|
||||
React.renderComponent(<TestComponent name="x" />, element);
|
||||
expect(mountCount).toEqual(1);
|
||||
|
||||
// Unmount and remount. We should get another mount event and
|
||||
// we should get different markup, as the IDs are unique each time.
|
||||
React.unmountAndReleaseReactRootNode(element);
|
||||
expect(element.innerHTML).toEqual('');
|
||||
React.renderComponent(<TestComponent name="x" />, element);
|
||||
expect(mountCount).toEqual(2);
|
||||
expect(element.innerHTML).not.toEqual(lastMarkup);
|
||||
|
||||
// Now kill the node and render it on top of the old markup, as if
|
||||
// we used server rendering. We should mount again, but the markup should be
|
||||
// unchanged.
|
||||
lastMarkup = element.innerHTML;
|
||||
React.unmountAndReleaseReactRootNode(element);
|
||||
expect(element.innerHTML).toEqual('');
|
||||
element.innerHTML = lastMarkup;
|
||||
// NOTE: we pass a different name here. This is to ensure that the markup
|
||||
// being generated is not replaced.
|
||||
var instance = React.renderComponent(<TestComponent name="y" />, element);
|
||||
expect(mountCount).toEqual(3);
|
||||
expect(element.innerHTML).toEqual(lastMarkup);
|
||||
|
||||
// Ensure the events system works
|
||||
expect(numClicks).toEqual(0);
|
||||
ReactTestUtils.Simulate.click(instance.refs.span.getDOMNode());
|
||||
expect(numClicks).toEqual(1);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user