Building an isomorphic react application

by Rachel Newstead

As someone new to the javascript and nodejs community, being asked to build an isomorphic React application was quite daunting. To be honest, I didn’t really know what this would entail.

However, I do like a challenge and this task certainly presented me with many of them. The first was to find out what an isomorphic application actually was. I’d come across React before (it’s a javascript web framework built by the team at Facebook) and was looking forward to using it again. I like how it provides a simple and clean way of defining components (although my frame of reference for javascript frameworks is limited: React is the only one I’ve used). The term isomorphic was more of a mystery to me and required some research before I could start playing with any code.

Isomorphic

In an isomorphic application, state is shared between the client and the server. There are a host of benefits to doing this. I found a very useful blogpost on Strongloop about why this is important: see How to Implement Node + React Isomorphic JavaScript & Why it Matters.

My key takeaways on why making your application isomorphic is important are:

  • An isomorphic application loads faster (or at least it appears to from a user’s point of view) as the full markup is available in the browser. This means even if the user has a slow connection they can still see the webpage immediately and don’t have to wait for the application to bootstrap,
  • You get better search engine visibility as all your javascript is available to be indexed by search engines,
  • You can present a working (or at least, not completely broken) application for users who have disabled javascript.

Through some trial and error (and lots of help from my very patient colleagues), I built a simple isomorphic application using the React.js framework. The application side of things should be a useful starting point for anyone building an isomorphic React application from scratch, although you would need to do more set up than this explanation provides. Some of the infrastructure and configuration I talk about here is specific to how things are set up at Tes, but hopefully it will be clear why something was needed to be done, so it will be easy enough to work out an alternative.

Create a service

At Tes we have some templates to generate the skeleton of a service using Bosco. From a template I generated a basic architecture for a nodejs application with a GET endpoint. Requests to this endpoint are handled by a controller.js, and this will be the starting point for introducing iso and React.

First, some dependencies are needed in package.json: the React framework and React-DOM, and Iso to share state between the server and client.

"iso": "^4.2.0",
"react": "^0.14.7",
"react-dom": "^0.14.3"

Make it isomorphic

In the controller we import both Iso and React. We create a new Iso object and configure it with markupClassName and dataClassName (these should be something specific to your React component). We pass both the markup and the state to the Iso object. When everything is wired up we will be able to view the two classes (__iso__markup__component and __iso__state__component) along with their associated markup and state in the DOM when we visit the application in a browser.

src/app/controllers/controller.js:

import React from 'react';
import ReactDom from 'react-dom/server';
import Iso from 'iso';

import Component from '../ui/components/IsomorphicComponent';

export default () => ({
  handle: (req, res) => {
    const iso = new Iso({
      markupClassName: '__iso__markup__component',
      dataClassName: '__iso__state__component',
    });

    const props = { state: "Some state shared between client and server" };

    const element = React.createElement(Component, props);
    const markup = ReactDom.renderToString(element);
    iso.add(markup, props);

    return res.send(` ${iso.render()} <script src="${req.headers['x-cdn-url']}js/component.js" async defer></script> `);
  },
});

There is some special Tes magic in <script src="${req.headers['x-cdn-url']}js/component.js" async defer></script>. We use a tool called Service-Page-Composer (built on Compoxure) to serve assets. The script tag above defines the javascript for the client (component.js) which will be served by Service-Page-Composer. The url for Service-Page-Composer is retrieved from the request headers: ${req.headers['x-cdn-url']}. However if you’re not using Service-Page-Composer this can be wherever you can serve a static javascript file from.

Next we have to create the component.js file. This will act as the entry point to the application and we will later set it up to be browserified and served by Service-Page-Composer.

It is in component.js that we get the state from the server and pass it to the client.

The first argument to Iso.bootstrap is a function that is called with the data from the server and a reference to its DOM container. In the example below this function returns a rendered React component. We pass the state from the server to the component as React props in React.createElement.

The second argument to Iso.bootstrap is a function that returns the Iso configuration as we defined it in controller.js.

src/app/public/js/react/component.js

import React from 'react';
import Iso from 'iso';
import ReactDOM from 'react-dom';
import Component from '../../../app/ui/components/IsomorphicComponent';

process.env.BROWSER = true;
Iso.bootstrap((state, meta, container) => {
  const component = React.createElement(Component, state);

  ReactDOM.render(component, container, meta.iso);
}, {
  markupClassName: '__iso__markup__component',
  dataClassName: '__iso__state__component',
});

Finally we create a React component using jsx. In this component we can access the state from the server via this.props.

src/app/ui/components/IsomorphicComponent.js

import React, { Component, PropTypes } from 'react';

class IsomorphicComponent extends Component {

  constructor(props) {
    super(props);
  }

  render() {
  return (
      <div className="component-container" style={{border: "1px solid red"}}>
        {this.props.state}
      </div>
    );
  }
}

export default IsomorphicComponent;

Tying it all together with Bosco, Service-Page-Composer and Nginx

In the service

In our gulpfile we set up the entry point to the application (component.js) to be browserified. This will inline all the dependencies required in the file (and the dependencies of their dependencies, and so on) so that everything is available to the browser. As we’re using es6 we also have to do an additional step to babelify the javascript. Not all browsers understand es6 yet, so babelify transforms es6 syntax into ECMAScript 5, which can be understood by all browsers.

import Gulp from 'gulp';
import VinylSourceStream from 'vinyl-source-stream';
import browserify from 'browserify';
import babelify from 'babelify';

import rename from 'gulp-rename';
import es from 'event-stream';
import glob from 'glob';

Gulp.task('build', (done) => {
  glob('src/public/js/react/**.js', (err, filePaths) => {
    if (err) { return done(err); }

    const files = filePaths.map((file) =>
      browserify({ entries: file, extensions: ['.js', '.jsx'], debug: true })
        .transform(babelify.configure({
          presets: ['es2015', 'react'],
          plugins: ['syntax-class-properties', 'transform-class-properties'],
        }))
        .bundle()
        .pipe(new VinylSourceStream(file))
        .pipe(rename({ dirname: '' }))
        .pipe(Gulp.dest('dist/js')));

    es.merge(files).on('end', done);
  });
});

The browserified file is put in dist/js. If you run gulp build you will be able to see the built component.js here, and that it’s a huge file containing all of our code and any dependencies, like React and Iso.

We add an npm target to package.json: "build-assets": "babel-node ./node_modules/gulp/bin/gulp.js build".

We use Bosco to run our services so we add a new target to serve the browserified file in bosco-service.json. This means the file will be served when we run bosco cdn.

"build": {
  "command": "npm run build-assets",
},
"assets": {
  "basePath": "/dist",
  "js": {
    "application": [
      "js/component.js"
    ]
  }
}

The following configurations needed for Service-Page-Composer and Nginx are specific to the microservice set up we have at Tes. You could achieve the same thing by serving the file statically in some other way.

In Service-Page-Composer

The javascript file gets served by Service-Page-Composer. We configure Service-Page-Composer with the location of the new service so that it knows where to serve the file from.

In Nginx

We have a microservice architecture and use Nginx to route requests to different services according to the request path. To be able to use the javascript file served by Service-Page-Composer we have to configure Nginx to route any requests for this file to Service-Page-Composer.

We can also add additional nginx configuration to enable us to visit the page on port 8080 (where we run the website locally).

To find out more about how we set up microservices at Tes, check out our CTO, Clifton Cunningham’s talk from NodeConf.

The output

Once this is all wired up and Service-Page-Composer and Nginx have been restarted with the new configuration, we can visit the service in a browser to see it working. (I hope the ugliness of the page doesn’t offend anyone, I deliberatly kept it as simple as possible!)

Visiting the service in a browser

We can see the state from the server is rendered in our browser.

When we inspect the DOM we can see the classes for both the iso markup and the iso state, and that these classes contain the markup and state passed from the server.