Strategies for server-side rendering of asynchronously initialized React.js components

node.jsReactjsFluxReactjs Flux

node.js Problem Overview


One of the biggest advantages of React.js is supposed to be server-side rendering. The problem is that the key function React.renderComponentToString() is synchronous which makes it impossible to load any asynchronous data as the component hierarchy is rendered on the server.

Let's say I have a universal component for commenting which I can drop pretty much anywhere on the page. It has only one property, some kind of identifier (for example id of an article below which the comments are placed), and everything else is handled by the component itself (loading, adding, managing comments).

I really like the Flux architecture because it makes a lot of things much easier, and its stores are perfect for sharing state between server and client. Once my store containing comments is initialized, I can just serialize it and send it from server to client where it is easily restored.

The question is what is the best way to populate my store. During past days I've been googling a lot and I've come across few strategies, none of which seemed really good considering how much this feature of React is being "promoted".

  1. In my opinion, the simplest way is to populate all my stores before the actual rendering begins. That means somewhere outside of the component hierarchy (hooked to my router for example). The problem with this approach is that I would have to pretty much define the page structure twice. Consider a more complex page, for example a blog page with many different components (actual blog post, comments, related posts, newest posts, twitter stream...). I would have to design the page structure using React components and then somewhere else I would have to define the process of populating each required store for this current page. That doesn't seem like a nice solution to me. Unfortunately most isomorphic tutorials are designed this way (for example this great flux-tutorial).

  2. React-async. This approach is perfect. It lets me simply define in a special function in each component how to initialize the state (doesn't matter whether synchronously or asynchronously) and these functions are called as the hierarchy is being rendered to HTML. It works in a way that a component is not rendered until the state is completely initialized. The problem is that it requires Fibers which is, as far as I understand, a Node.js extension that alters the standard JavaScript behavior. Although I really like the result, it still seems to me that instead of finding a solution we changed the rules of the game. And I think we shouldn't be forced to do that to use this core feature of React.js. I'm also not sure about the general support of this solution. Is it possible to use Fiber on standard Node.js web hosting?

  3. I was thinking a little on my own. I haven't really thought trough the implementation details but the general idea is that I would extend the components in similar way to React-async and then I would repeatedly call React.renderComponentToString() on the root component. During each pass I would collect the extending callbacks and then call them at the and of the pass to populate the stores. I would repeat this step until all stores required by current component hierarchy would be populated. There are many things to be solved and I'm particularly unsure about the performance.

Did I miss something? Is there another approach/solution? Right now I'm thinking about going the react-async/fibers way but I'm not completely sure about it as explained in the second point.

Related discussion on GitHub. Apparently, there is no official approach or even solution. Maybe the real question is how the React components are intended to be used. Like simple view layer (pretty much my suggestion number one) or like real independent and standalone components?

node.js Solutions


Solution 1 - node.js

If you use react-router, you can just define a willTransitionTo methods in components, which gets passed a Transition object that you can call .wait on.

It doesn't matter if renderToString is synchronous because the callback to Router.run will not be called until all .waited promises are resolved, so by the time renderToString is called in the middleware you could have populated the stores. Even if the stores are singletons you can just set their data temporarily just-in-time before the synchronous rendering call and the component will see it.

Example of middleware:

var Router = require('react-router');
var React = require("react");
var url = require("fast-url-parser");

module.exports = function(routes) {
    return function(req, res, next) {
        var path = url.parse(req.url).pathname;
        if (/^\/?api/i.test(path)) {
            return next();
        }
        Router.run(routes, path, function(Handler, state) {
            var markup = React.renderToString(<Handler routerState={state} />);
            var locals = {markup: markup};
            res.render("layouts/main", locals);
        });
    };
};

The routes object (which describes the routes hierarchy) is shared verbatim with client and server

Solution 2 - node.js

I know this is probably not exactly what you want, and it might not make sense, but I remember getting by with slighly modifying the component to handle both :

  • rendering on the server side, with all the initial state already retrieved, asynchronously if needed)
  • rendering on the client side, with ajax if needed

So something like :

/** @jsx React.DOM */

var UserGist = React.createClass({
  getInitialState: function() {

    if (this.props.serverSide) {
       return this.props.initialState;
    } else {
      return {
        username: '',
        lastGistUrl: ''
      };
    }

  },

  componentDidMount: function() {
    if (!this.props.serverSide) {
    
     $.get(this.props.source, function(result) {
      var lastGist = result[0];
      if (this.isMounted()) {
        this.setState({
          username: lastGist.owner.login,
          lastGistUrl: lastGist.html_url
        });
      }
    }.bind(this));

    }
    
  },

  render: function() {
    return (
      <div>
        {this.state.username}'s last gist is
        <a href={this.state.lastGistUrl}>here</a>.
      </div>
    );
  }
});

// On the client side
React.renderComponent(
  <UserGist source="https://api.github.com/users/octocat/gists" />,
  mountNode
);

// On the server side
getTheInitialState().then(function (initialState) {

    var renderingOptions = {
        initialState : initialState;
        serverSide : true;
    };
    var str = Xxx.renderComponentAsString( ... renderingOptions ...)  

});

I'm sorry I don't have the exact code at hand, so this might not work out of the box, but I'm posting in the interest of discussion.

Again, the idea is to treat most of the component as a dumb view, and deal with fetching data as much as possible out of the component.

Solution 3 - node.js

I was really messed around with this today, and although this is not an answer to your problem, I have used this approach. I wanted to use Express for routing rather than React Router, and I didn't want to use Fibers as I didn't need threading support in node.

So I just made a decision that for initial data which needs to be rendered to the flux store on load, I will perform an AJAX request and pass the initial data into the store

I was using Fluxxor for this example.

So on my express route, in this case a /products route:

var request = require('superagent');
var url = 'http://myendpoint/api/product?category=FI';

request
  .get(url)
  .end(function(err, response){
    if (response.ok) {    
      render(res, response.body);        
    } else {
      render(res, 'error getting initial product data');
    }
 }.bind(this));

Then my initialize render method which passes the data to the store.

var render = function (res, products) {
  var stores = { 
    productStore: new productStore({category: category, products: products }),
    categoryStore: new categoryStore()
  };

  var actions = { 
    productActions: productActions,
    categoryActions: categoryActions
  };

  var flux = new Fluxxor.Flux(stores, actions);

  var App = React.createClass({
    render: function() {
      return (
          <Product flux={flux} />
      );
    }
  });

  var ProductApp = React.createFactory(App);
  var html = React.renderToString(ProductApp());
  // using ejs for templating here, could use something else
  res.render('product-view.ejs', { app: html });

Solution 4 - node.js

I know this question was asked a year ago but we had the same problem and we solve it with nested promises that were derived from the components that are going to be render. In the end we had the all data for the app and just sent it down the way.

For example:

var App = React.createClass({

	/**
     *
     */
    statics: {
        /**
         *
         * @returns {*}
         */
        getData: function (t, user) {

            return Q.all([

                Feed.getData(t),

                Header.getData(user),

                Footer.getData()

            ]).spread(
                /**
                 *
                 * @param feedData
                 * @param headerData
                 * @param footerData
                 */
                function (feedData, headerData, footerData) {

                    return {
                    	header: headerData,
                    	feed: feedData,
                    	footer: footerData
                    }

                });

        }
    },

    /**
     *
     * @returns {XML}
     */
    render: function () {

        return (
            <label>
                <Header data={this.props.header} />
                <Feed data={this.props.feed}/>
                <Footer data={this.props.footer} />
            </label>
        );

    }

});

and in the router

var AppFactory = React.createFactory(App);

App.getData(t, user).then(
    /**
     *
     * @param data
     */
    function (data) {

        var app = React.renderToString(
            AppFactory(data)
        );       

        res.render(
            'layout',
            {
                body: app,
                someData: JSON.stringify(data)                
            }
        );

    }
).fail(
    /**
     *
     * @param error
     */
    function (error) {
        next(error);
    }
);

Solution 5 - node.js

Wanna share with you my approach of server side rendering using Flux, little be simplified for example:

  1. Let's say we have component with initial data from store:

     class MyComponent extends Component {
       constructor(props) {
         super(props);
         this.state = {
           data: myStore.getData()
         };
       }
     }
    
  2. If class require some preloaded data for initial state let's create Loader for MyComponent:

      class MyComponentLoader {
         constructor() {
             myStore.addChangeListener(this.onFetch);
         }
         load() {
             return new Promise((resolve, reject) => {
                 this.resolve = resolve;
                 myActions.getInitialData(); 
             });
         }
         onFetch = () => this.resolve(data);
     }
    
  3. Store:

     class MyStore extends StoreBase {
         constructor() {
             switch(action => {
                 case 'GET_INITIAL_DATA':
                 this.yourFetchFunction()
                     .then(response => {
                         this.data = response;
                         this.emitChange();
                      });
                  break;
         }
         getData = () => this.data;
     }
    
  4. Now just load data in router:

     on('/my-route', async () => {
         await new MyComponentLoader().load();
         return <MyComponent/>;
     });
    

Solution 6 - node.js

just as a short rollup -> GraphQL will solve this entierly for your stack...

  • add GraphQL
  • use apollo and react-apollo
  • use "getDataFromTree" before you start rendering

-> getDataFromTree will automatically find all the involved queries in your app and execute them, pouplating your apollo cache on the server and thus, enabling fully working SSR.. BÄM

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestiontobikView Question on Stackoverflow
Solution 1 - node.jsEsailijaView Answer on Stackoverflow
Solution 2 - node.jsphtrivierView Answer on Stackoverflow
Solution 3 - node.jssvnmView Answer on Stackoverflow
Solution 4 - node.jsRotemView Answer on Stackoverflow
Solution 5 - node.jszooblinView Answer on Stackoverflow
Solution 6 - node.jsjebbieView Answer on Stackoverflow