Building robust web apps with React: Part 4, server-side rendering

— Updated

A few months ago I wrote the first part of this series and I was really excited about the possibilities of using React to create intelligent applications that could avoid the frailties many current JavaScript apps have. At last I’m going to make my Tube Tracker application run first on the server and then run the same code in the browser, completing the loop of the isomorphic or adaptive-hybrid app.

In the last part of this series I was wandering into the unknown finding a testing strategy and this final part is not dissimilar, although there are articles and basic demos about isomorphic JavaScript there are not many Open Source implementations to study. Implementation details aside, the biggest change, for me, from writing JavaScript on the client-side is having data first.

Re-organising data flow

The in-browser versions of my Tube Tracker application have a very simple data-flow but the model doesn’t quite translate to the server. In the browser data must be fetched via an AJAX request once the app has loaded but an isomorphic app should deliver the complete HTML to the browser.

A React application has one point of access to the component stack–the root either rendered with render or renderToString–so data must be provided to the top of the stack and passed down through it. This is contrary to the usual thinking when working with React because data should only be handled by the components that need it.

The data required and flow through the application, including initial data payload

As the data passes through the stack the components that originally needed to go and fetch data can utilise their getInitialState lifecycle method to preset and use the data straight away. The rest of the component should remain mostly untouched, only now modified to skip the initial loading state.

var Predictions = React.createClass({
  getInitialState: function() {
    return {
      status: this.props.initialData ? "success" : "welcome",
      predictionData: this.props.initialData
    };
  },
  ...
});
~/app/component/predictions.jsx

The data being provided to the application root on the server also needs to be implemented on the client-side, if the app were to load in the browser without the same data being provided then it would be re-rendered into its initial, empty state. The most straightforward way to transfer the data onto the client-side is to render it into a script element for the application bootstrap to pick up:

var React = require("react");
var networkData = require("../common/data");
var TubeTracker = require("../component/tube-tracker.jsx");

window.app = (function() {
  ...
  var initialData = JSON.parse(document.getElementById("initial-data").innerHTML);
  return React.render(<TubeTracker networkData={networkData} initialData={initialData} />, document.body);
})();
~/app/browser/bootstrap.jsx

React on the server

Components using the JSX syntax must be transformed into plain JavaScript before use but on the server but that doesn’t necessarily require a pre-compilation step. React Tools can perform a just-in-time transformation and it’s conveniently been wrapped up into the Node-JSX package which can transparently interpret modules as they’re required. Node-JSX only needs including once in the application because require is global but use it with caution: the parser used internally by React Tools may baulk at some of the more creative uses of JavaScript, so for safety and performance it is best to distinguish component files with a .jsx extension.

require("node-jsx").install({ extension: ".jsx" });

var config = require("./config");
var express = require("express");
var API = require("./app/server/api");
var Bootstrap = require("./app/server/bootstrap.jsx");
~/server.jsx

The component stack is rendered just as it is within the browser but instead of creating a dynamic tree, only a string of HTML is required. React provides the top-level method renderToString for this purpose and it will only run each component’s getInitialState and componentWillMount lifecycle methods. Components should translate to the server without causing problems so long as no browser-specific code is executed, so make sure any client-side setup is moved into the componentDidMount method.

The rest on the server

The final few steps for delivering the initial HTML to the browser are implementation specific but for reference I’ll quickly cover the setup behind the Tube Tracker app.

The application already uses Express to deliver static assets so an additional route was added to process the request and respond with the static HTML. The route makes an API request if necessary and pushes any data returned into a template loaded from the file system:

app.get("/", function(req, res) {
  new API(config).for(req.query.line, req.query.station).get(function(err, data) {
    if (err) {
      return res.send(500, "API error");
    }

    new Bootstrap(data).load(function(err, responseHTML) {
      if (err) {
        return res.send(500, "Template error");
      }

      res.send(responseHTML);
    });
  });
});
~/server.jsx

The template module is extremely basic, it will load the requested file from the file system and replace any named placeholders with the given data. There was no need to employ a more complex templating library because there will only ever be the two simple pieces of data to inject:

var fs = require("fs");
var path = require("path");

function Template(target) {
  this.target = target;
}

Template.prototype.render = function(data, callback) {
  var fullPath = path.resolve(__dirname, this.target);

  fs.readFile(fullPath, { encoding: "utf8" }, function(err, template) {
    if (err) {
      return callback(err);
    }

    var rendered = template.replace(/\{\{yield:([a-z0-9_]+)\}\}/g, function(match, property) {
      return data[property];
    });

    callback(null, rendered);
  });
};

module.exports = Template;
~/app/server/template.js

It is no longer possible to create a complete HTML document using React components due to incompatibility between browsers. That may not affect React running on the server but it would be strange to create a component that would not be shared in this context.

Conclusion

Building an application to run on both the server and in the browser has been an interesting journey. React is amazing tool for building client-side apps and it enables tremendous productivity as dynamic applications are very easy to compose. With careful planning it can make the brittle, client-side JavaScript app into a robust, reliable product. However, I am reluctant to evangelise about isomorphic JavaScript applications at this time; the added development complexity made building the Tube Tracker app a chore. The browser-only version took just a few hours to build and the server version even less, but structuring and abstracting the code to work as one took many times longer than anticipated.

A venn diagram showing how much code is shared between the server and browser

Relatively little of the project’s code is shared between the two environments, only well-abstracted utilities and the React components have made the transition.

No doubt the time spent developing isomorphic apps will come down as the processes become more commonplace and new patterns emerge but right now it worries me that the extra effort involved is actually just developer indulgence. If the application being rendered on the server is fully featured then there needs to be a really convincing argument that a user’s experience is improved by making them download and execute a large amount of JavaScript which only re-implements the application they already have.

As web developers our efforts to mimic the behaviour of smart phone apps have led to a frenzied evolution of the technologies we use and we’ve learnt a great deal about the web as a platform for delivering all manner of different content. Some of the new features and techniques–like isomorphic JavaScript–that have emerged are very interesting but building new products for the web continues to become more complicated.

I really like React, it’s a brilliant tool, but I like it much more when it’s kept simple.


You can test the completed app right now (note: It’s running on a free account so this link may not be reliable) or head over to GitHub to check out the source code. Please leave a comment or send me a tweet, I’d love some feedback.

View the project on GitHub

comments powered by Disqus

A photo of Matt Hinchliffe

About Me

I'm a 28 year old front-end developer building new stuff at the Financial Times based in London. I specialise in crafting scalable, performance-driven code, tackle accessibility issues and keep an opinionated interest in the latest hotness. I like my tea robustly brewed, white and with no sugar, thanks!