-
Notifications
You must be signed in to change notification settings - Fork 72
Asynchronous server-side rendering (follow-up of Stackoverflow/Twitter discussion) #47
Comments
There's a couple of approaches that can be taken. Some people have already been experimenting with doing all of the data fetching at the router layer. This is actually pretty good for many application styles (I'm thinking, more "railsy" apps). But when building large applications with a huge team or company, I feel that we need the data fetching to be closer to the React component level. Here's a couple of things I'd like to hear your thoughts on:
Some of the things we will want to try involve making modifications to the React core. For example, we need to be able to "pause" and resume a rendering. Luckily @ben Alpert created something called Believe it or not, this is a very important problem that we care a lot about for many reasons. I believe a really good open sourced client/server non-blocking data fetching/routing system for React would provide an unprecedentedly efficient developer experience. If you build a solution, it would be great if you could share it. We can help you figure out what would need to be done to the React core. |
The most straightforward (although probably not the most easy to implement) solution would be to make the lifecycles functions asynchronous, and in particular getInitialState. If getInitialState is asynchronous, then the component can asynchronously initialize itself with only one rendering pass. The problem is of course that since getInitialState is deeply nested in the rendering stack, all the stack must be made asynchronous, too. In particular, React expressions returned by ReactComponent.render must be handled asynchronously or lazily. render: function() {
return AsyncComponent() + SyncComponent(); // should return a promise for [the future result of AsyncComponent()] + SyncComponent() ?
} Which asynchronous pattern should be used is not the core issue, of course (though I have a personal taste for promises, React API seems to be more designed around callbacks). Another, less seducing option would be to maintain 2 trees : a React component tree, purely synchronous, and a data dependency tree, asynchronous, that is build prior to rendering the root of the component tree. The data dependency may be expressed in terms of functions in the component definition, and resolved dynamically when required. Its less beautiful but requires less modifications in the React core code. I'm very glad that you are aware of the problem and open to discussing solutions. |
Your proposals for async versions of I honestly believe that your second "less seducing" option is the more practical approach. |
I think the second option is better too. Imagine you have a hierarchy where A owns B and B owns C. If Another problem with making |
@jordwalke👍 You have a very good point, regarding the return type of render, my example is bad; whenever @petehunt: I think serial dependencies are precisely what this problem is all about. If all dependencies can be managed in parallel, then it can be (relatively easily) be worked around at the top level. Problems appear when a complex hierarchy of components yields serial dependencies. The example from http://facebook.github.io/react/docs/tutorial.html is perfect, if for example instead of very simplistic 'author' field, you have a more realistic 'authorId' field, and you need to fetch additional data for a subcomponent "Author" of "Comment". The data dependency is so that you cant fetch the author data unless you have the authorId, which you can't have before you have fetched the initial comment list. I think the serial complexity is intrinsic, and often is when designing complex apps , as @jordwalke seemed to agree with, if you want the data fetching relatively close to its view (in terms of a React component). Today, this can be done on the client, but requires heavy work on the server (such as dummy-calling renderComponentToString repeatedly until "nothing is to be done", etc). I'm a bit afraid I'm trying to reinvent the wheel here. Please feel free to share with me if I've misunderstood a core concept of React or if you have experienced pratical way of managing this kind of asynchronous dependencies on the server. |
I've been giving this topic alot of thinking for the past several days... :) The problem is the following:
For the record, here is the code of the little module and an extremely simple use case (with only 1 component): var React = require("react");
var ReactAsync = {};
ReactAsync.Handlers = {};
ReactAsync.Mixin = {
propTypes: {
asyncHandler: function(props, propName, componentName) {
if(!ReactAsync.Handlers[props[propName]]) {
throw new Error("Invalid asynchronous handler");
}
}
},
asyncHandlerActive: function() {
return !!ReactAsync.Handlers[this.props.asyncHandler];
},
setPending: function() {
if(this.asyncHandlerActive()) {
ReactAsync.Handlers[this.props.asyncHandler].pending(this);
}
},
setReady: function() {
if(this.asyncHandlerActive()) {
ReactAsync.Handlers[this.props.asyncHandler].ready(this);
}
}
};
var without = function(array, el) { // Utility function, returns a copy of array without instance of el
var arrayWithout = [];
array.forEach(function (e) {
if(e !== el) {
arrayWithout.push(e);
}
});
return arrayWithout;
};
var uniqueId = (function() {
var id = 0;
return function() {
return ++id;
};
})();
ReactAsync.renderComponentToString = function (RootComponent, props, callback) {
console.warn("ReactAsync::renderComponentToString");
var id = uniqueId();
var newProps = {};
for(var propName in props) {
if(props.hasOwnProperty(propName)) {
newProps[propName] = props[propName];
}
}
newProps.asyncHandler = id;
var rootComponent = RootComponent(newProps);
var cleanUpAndCallback = function() {
console.warn("cleanUpAndCallback");
React.renderComponentToString(rootComponent, function (html) {
delete ReactAsync.Handlers[id];
callback(html);
});
};
var pendingComponents = [];
ReactAsync.Handlers[id] = {
pending: function(component) {
pendingComponents.push(component); // Add a component to the checklist
console.warn("pending::pendingComponents: " + pendingComponents.length);
},
ready: function(component) {
pendingComponents = without(pendingComponents, component); // Remove a component from the checklist
console.warn("ready::pendingComponents:" + pendingComponents.length);
if (pendingComponents.length === 0) { // In case there is nothing more to do, immediatly return
cleanUpAndCallback();
}
}
};
React.renderComponentToString(rootComponent, function (html) {
if (pendingComponents.length === 0) { // In case there is nothing to do, immediately return
cleanUpAndCallback();
}
});
};
module.exports = ReactAsync; /** @jsx React.DOM */
var React = require("react");
var ReactAsync = require("../react-async.js");
module.exports = React.createClass({
mixins: [ReactAsync.Mixin],
getInitialState: function() {
console.warn("Test::getInitialState");
return {
foo: "initial value"
}
},
componentWillMount: function() {
console.warn("Test::componentWillMount", this.props, this.state);
this.setPending();
setTimeout(function() {
console.warn("entering setState");
try {
this.setState({
foo: "async value"
});
}
catch(e) {
console.warn("caught:", e);
}
console.warn("setState done");
this.setReady();
console.warn("setReady done");
}.bind(this), 500);
},
render: function() {
console.warn("Test::render", this.props, this.state);
var r = React.DOM.span({}, this.state.foo);
console.warn("r ok");
return r;
}
}); I see several possibilities from here:
Other than this, I don't see any simple way of handling asynchronous components on the server. The possibility of pre-fetching all the data before rendering anything is still there, but not without breaking the symetry between what's happening on the server and what's happening on the client. |
Here is one way set up this sort of composable data fetching. It should work both on the client and the server. The problems with it are that it uses static methods in a hacky way and requires you to pass the data down and call fetch explicitly throughout the hierarchy. If you check out the undocumented/experimental context feature you will see a fix this. |
I would love to help out with this effort. Just from my naive interpretation of nomenclature (read: I'm not diving into react src yet...), it seems that mounting and rendering are somewhat different concepts. Please correct me if this is wrong: renderComponent "mounting" is the process of injecting the virtual DOM into the document, renderComponentToString "mounting" is the construction of the html string. "Rendering" in either case is the construction of the virtual DOM? If the above is accurate, I might suggest the following async component API: var Todo = React.createClass({
getInitialState: function() {
return {todo: {}};
},
componentWillMount: function() {
var until = this.deferMount(),
self = this;
http.get('/api/todos/' + this.props.todoId, function(todo) {
self.setState({todo: todo});
until();
}
},
render: function() {
return (
<h2>{this.state.todo.title}</h2>
);
}
}); So React would continue with the render (which I assume means building the virtual DOM based on calls to components' render() funcs), despite the call to deferMount. The A - B - C dependency chain @petehunt mentioned would not cause a holdup, since the render() funcs would be called immediately for each component. However, React will delay insertion into the document (or construction of the html string) until all until() callbacks are called (ha).
Again, I'm not sure how this jives with current React src/paradigms, so I hope this is in some way helpful. I'm happy to get my feet wet. I'm lovin React btw. |
bump |
This project is not actively maintained. We have no intentions of making further changes beyond super basic maintenance. |
This is a follow-up from http://stackoverflow.com/questions/20815423/how-to-render-asynchronously-initialized-components-on-the-server & https://twitter.com/elierotenberg/status/418358101880762368
Practical single-page apps are often rendered on the client asynchronously, usually dynamically issuing ajax requests to load parts of the content. Even on the server side, the full rendering often relies on asynchronous operations following node's non blocking, asynchronous paradigm, e.g. db queries or delegated services implementing a network API (REST or w/e). In addition, if the data/models are exposed via an asynchronous web service (usually ajax, or websockets), asynchronously rendered page can induce significant latency due to network roundtrips, which can be effectively handled by the server (either by ensuring that the server operating the rendering is efficently connected to the data service and/or by maintaining a data cache on the server).
I think the right place to initiate asynchronous loading is in ReactComponent.componentWillMount (since its called when using React.renderComponentToString prior to calling ReactComponent.render while React.componentDidMount isn't). However, React.renderComponentToString and the underlying rendering stack is synchronous; which means there is no simple way (or atleast I couldn't think of one) to perform complete server-side rendering of asynchronously-rendered pages. It might be worked around using a "global" per-request synchronization object containing the promises of the asynchronous operations involved, but I've found it to be tricky at best. Supporting asynchronous rendering would allow practical SPA to be rendered on the server.
The text was updated successfully, but these errors were encountered: