Skip to content
This repository has been archived by the owner on Jan 13, 2022. It is now read-only.

Asynchronous server-side rendering (follow-up of Stackoverflow/Twitter discussion) #47

Closed
elierotenberg opened this issue Jan 1, 2014 · 10 comments

Comments

@elierotenberg
Copy link

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.

@jordwalke
Copy link
Contributor

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:

  1. How should nested components be handled when they all want to fetch data. If we supported React components fetching data, we'd need to block not only the top component, but deeper ones as well. This could get out of hand (or maybe not) - but technically, it might be easiest to start with a simple limitation that only the top layer of components can perform data fetching
  2. The beauty of React is that you write your code once and that description is sufficient to carry out initial rendering as well as updates. What should be done when some state updates in a component and the component needs to refetch data. Now that we're on the client, we need the components to be able to refetch any needed data, which would involve multiple asynchronous stages if the result of one query is an input to another query. These round trips aren't quite so bad when you're on the server, but are very slow when done on the client. There are some very interesting problems to solve here. React is uniquely positioned as a framework that can support these solutions, but they need to be built. For example, one of React's principles is that a component tree should be serializable. So in theory, you could serialize enough information to be able to send it back to the server first before executing the multiple round trips. Alternatively, you can impose certain limitations on the types of queries that can be performed, and the way in which inputs to queries can be influenced by previous query results. It's one of the most fascinating research areas - I haven't seen any solutions yet.

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 _pendingProps and _pendingState inside of React components that might allow us to pause initial rendering and updates.

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.

@elierotenberg
Copy link
Author

  1. I think allowing only the top level components to fetch data is only a very slight improvement since, as I said, for simple cases, it can be worked around manually, eg. by pre-fetching and passing data as prop, or calling renderComponentToString twice (once to initialize the component and trigger the fetching, the second when the fetching is done to perform the final render).
  2. I agree that one of the most beautiful aspects of React is its descriptive/declarative approach and the use of pure functions for rendering, which are made possible by serialization (among other principles). In this mindset, a router may associate a request (in particular, a route, but also several appstate parameters such as session id or cookie) to a top-level React component as a set of props. This set of props being serializable, the client may defer the rendering of parts or all of the page to the server to reduce network round-trips.

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.

@jordwalke
Copy link
Contributor

Your proposals for async versions of getInitialState seem reasonable, but I don't think that the effect of an async API would ever cause your render return values to be of a different nature (they'd never need to be promises - they can remain regular React components). Every React component, such as <YourTypeahead /> is simply a description of what should be rendered. Currently, another system is responsible for taking these descriptions and actually rendering/updating the actual backing components. That system is the one that should be made asynchronous. Ideally, no component's render function would be invoked until the necessary responses have already been obtained.

I honestly believe that your second "less seducing" option is the more practical approach.

@petehunt
Copy link
Contributor

petehunt commented Jan 2, 2014

I think the second option is better too. Imagine you have a hierarchy where A owns B and B owns C. If getInitialState() were async (I assume in order to do a roundtrip to the server) and we had to do a fetch for each of these components we'd need to wait for 3 serial round trips before being done rendering because we can't start fetching the child until the parent renders. If we go with your second approach we can analyze the data hierarchy and batch these up more easily.

Another problem with making getInitialState() async is refs -- what happens if you want to query a ref before it has rendered? Solvable, but requires a lot of work and thinking.

@elierotenberg
Copy link
Author

@jordwalke👍 You have a very good point, regarding the return type of render, my example is bad; whenever render is called, all the data from the (parent) component has been fetched, therefore the description of the child is complete (since it only depends on the props and (asyncly initialized) state of the (parent) component; when it is actually instantiated and populated with async data is not to be dealt within render. Thus, render need not be asynchronous. Only to be called whenever the component has reached its asyncly initialized state. This can be managed by maintaining a special _asyncInitalized state variable, initialized to false and set to true when the async init is done, to be checked in the render function, for example.

@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.

@elierotenberg
Copy link
Author

I've been giving this topic alot of thinking for the past several days... :)
I've written a little module as a proof of concept that uses a per-root-level-component handler that allows components to register themselves as pending/ready. The root level component is passed an asyncHandler prop, which it can pass to the components it owns. Its not very beautiful, since it requires explicit opt-in from the owner (passing its asyncHandler prop and using ReactAsync.Mixin) and the ownee (calling setPending and setReady) but in principle it should work.

The problem is the following:

  • getInitialState is meant to be pure, so the async initialization can not start there
  • componentDidMount is not called on the server, so the async initilization can not start there
  • componentWillMount seems to be the good place.
  • however, to make a component call its componentWillMount method, it must be mounted, via renderComponentToString on the server
  • when the component is ready (has called its setReady method from the mixin), it triggers a re-render (which is intented); however, since it has already been "mounted" in a string in the first call to renderComponentToString, it raises an exception (an obscure TypeError: Cannot read property 'firstChild' of undefined followed by an InvariantViolation stating that the component has already been mounted). I assume that when the component is rendered again, React expects it to be in an actual DOM, not in a string.

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:

  • Allow render to be called again, when its not actually mounted in the DOM but rendered in a string; I don't really see how thats feasible, however.
  • Expose a rendering primitive at a lower level than renderComponentToString; e.g. at the virtual DOM level, so that React can modify the object this primitive has returned at a later point (in subsequent calls of render), while still being able to compile it to a string when the server is ready to sent the response
  • Provide special lifecycle methods to prevent the initial rendering from actually happening before the async initialization is complete ?

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.

@petehunt
Copy link
Contributor

petehunt commented Jan 4, 2014

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.

http://jsfiddle.net/HWzJ6/

@bauerca
Copy link

bauerca commented Jan 11, 2014

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).

renderComponentToString is already async-friendly, given that it takes a callback. renderComponent could use a callback as a final optional argument. Or some kind of event trigger.

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.

@ghost
Copy link

ghost commented Oct 14, 2015

bump

@zpao
Copy link
Contributor

zpao commented Oct 14, 2015

This project is not actively maintained. We have no intentions of making further changes beyond super basic maintenance.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants