Strategies for server-side rendering of asynchronously initialized React.js components
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 .wait
ed 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
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 sideReact.renderComponent( <UserGist source="https://api.github.com/users/octocat/gists" />, mountNode);// On the server sidegetTheInitialState().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.
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 });