Universal React with Rails: Part III

react on railsAugust 01, 2015Dotby Alex Fedoseev

Building Universal app

It’s been awhile since I wrote my previous post about Rails API. Meantime a lot of things happened in JavaScript world: ES6 was officially approved and renamed to ES2015, “isomorphic” javascript (almost) became “universal”, one of the flux frameworks (which I’ve started to use in my apps) was deprecated in favour of new one (which we will use in the next post).

There is a great quote by Addy Osmani:

First do it, then do it right, then do it better.

Our goal is to learn how to build modern javascript apps and efficiently manage their state. But before do complex things, we should learn basics. So our starting point will be super simple app, which doesn’t interact with API and have only static data inside React components. In other words we will build static javascript application with server rendering using React.

:before

Here is the list of stuff you need to be familiar with to feel comfortable while reading this post:

Also take a look at these posts, we will use them later.

You can return to these links anytime, if you’ll stuck somewhere down the road.

The app

We gonna build small site with 2 pages: Main and About. Nothing special, except that it will be universal.

Source code @Github

Project structure

I always keep in mind that there can be multiple bundles in app (public site and admin area for example), so some modules will be split on two logical parts:

  • reusable abstraction (it will be shared between all bundles)
  • bundle-specific stuff.

This is how it usually looks like in my projects.


|-- /app                    <---- App container

|------ /assets             <---- Shared assets (css, fonts etc.)

|------ /bundles            <---- Bundles

|---------- /[bundle 1]

|---------- /[bundle 2]

|------ /errors             <---- Server errors layouts

|------ /libs               <---- Shared libs

|

|-- /build                  <---- Build configs

|-- /config                 <---- Application configs

|-- /public                 <---- Public assets

|-- /server                 <---- Express middlewares

|-- .dotfiles               <---- Dotfiles

|-- gulpfile.babel.js       <---- Gulpfile

|-- package.json            <---- You know what it is

|-- server.[bundle].js      <---- Bundle's Express server

|-- server.dev.js           <---- Webpack dev server

|-- server.js               <---- Reusable part for Express servers

Shared code base

Let’s start with the shared code, which will be the same on the server and on the client. Actually, it’s super simple React components with super simple react-router setup.

First of all we’ll define routes:

import React      from 'react';
import { Route }  from 'react-router';

import App        from '../layouts/Layout';
import Main       from '../components/Main';
import About      from '../components/About';
import NotFound   from '../components/NotFound';


export default (

    <Route name="app" component={App}>

        <Route name="main"        path="/"        component={Main} />
        <Route name="about"       path="/about"   component={About} />

        <Route name="not-found"   path="*"        component={NotFound} />

    </Route>

);

And this is Layout component:

/* app/bundles/app/layouts/Layout.jsx */

import React    from 'react';

import Header   from '../components/Header';

export default class Layout extends React.Component {
    constructor(props, context) {
        super(props, context);
    }

    render() {
        return (
            <section id="layout">
                <Header />
                {/* In react-router 1.0.0 there is no `RouteHandler` */}
                {/* It's just `this.props.children` instead */}
                {this.props.children}
            </section>
        );
    }
}

Main & About components are trivial — you can find them here.

Our main goal — make it universal. Let’s do it.

Server side rendering

Request to server (nginx isn’t shown, but it’s there)
Request to server (nginx isn’t shown, but it’s there)

When request from the browser gets to our app, it will be handled by Express — Node.js web app framework. Express server doesn’t know anything about React, it just takes request, parses it and passes it with response object to middleware function. This middleware takes care of the rest: initializes React app, renders initial html and sends it to browser (by response object from Express) with client’s javascript app on the board.

Express server:

/* server.app.js */

// This is entry point of our `app` bundle.
// And here we collect bundle specific stuff 
// to pass it to reusable abstraction part.
// We can add `server.admin.js` bundle etc.


// We gonna use ES6 / ES7 syntax in our modules
// So we need to transform it to ES5 with every `require` from here on
require('babel/register')({
  extensions: ['.js', '.jsx'],
  stage     : 0
});

// Middleware function of `app` bundle
var initter = require('./app/bundles/app/initters/server');

// Bundle settings
var config = require('./config/server.app');

// Starting express server
require('./server')(initter, config);
/* server.js */

// This is shared between all bundles

import express  from 'express';
import parser   from 'body-parser';
import cookies  from 'cookie-parser';
import path     from 'path';


export default (initter, config) => {
  
  // Defining some globals
  global.__CLIENT__ = false;
  global.__SERVER__ = true;
  global.__DEV__    = config.env !== 'production';

  // Initializing app
  const app = express();

  // Parsing json bodies
  app.use(parser.json());
  
  // Parsing urlencoded bodies
  app.use(parser.urlencoded({ extended: true }));

  // Parsing cookies
  app.use(cookies());

  // Serving static files from `public` dir
  app.use(express.static(path.join(__dirname, 'public')));

  // Here we are!
  // Transferring incoming requests to middleware function of the bundle
  app.use('/', initter);

  // Setting up port to listen
  app.set('port', config.appPort);

  // And listening it
  app.listen(app.get('port'), function() {
    console.log(`=> 🚀  Express ${config.bundle} ${config.env} server is running on port ${this.address().port}`);
  });

}

Config for app bundle:

/* config/server.app.js */

// Importing shared config part
import config from './server';

// Defining name of the bundle
config.bundle = 'app';

// Defining port of bundle
// In production we will store this in APP_PORT environment variable
// In development it will be 3500
config.appPort = process.env.APP_PORT || 3500;

export default config;
/* config/server.js */

// This is shared between all bundles

let config = {};

// Defining environment
config.env = process.env.NODE_ENV  || 'development';

// Defining port for webpack dev server (details below)
config.devPort = 3001;

export default config;

Express did all the routine job and passed request to bundle’s middleware (I call it initter), where all magic is gonna happen.

Middleware function in Express is function with following signature:

function middleware(req, res, next) {
  res.send("Hello, world!")
}

This is entry point of React app, and it also will be split on abstraction / bundle-specific parts:

/* app/bundles/app/initters/server.jsx */

// Here we collect bundle specific stuff 
// to pass it to shared initter with bundle `params`

import initter    from 'app/libs/initters/server';
import getAsset   from 'app/libs/getAsset';
import config     from 'config/server.app';
import routes     from '../routes/routes';
import Head       from '../layouts/Head';


export default (req, res, next) => {

  const { bundle } = config;

  const params = {

    // Name of the bundle
    bundle,
    
    // Routes
    routes,
    
    // <head> template
    Head,

    // Variables for Jade template
    // Here we store paths to compiled assets to inject them in html
    // Explore `getAsset` helper in `app/libs` folder
    locals: {

      jsAsset    : getAsset(bundle, 'js'),
      cssAsset   : getAsset(bundle, 'css'),
      vendorAsset: getAsset('vendor', 'js')

    }

  };

  // Initializing app
  initter(req, res, next, params);

}
/* app/libs/initters/server.jsx */

// This initter is shared between all bundles

import React      from 'react';
import Router     from 'react-router';
import Location   from 'react-router/lib/Location';
import serialize  from 'serialize-javascript';
import jade       from 'jade';


export default (req, res, next, params) => {

  // Storing params in variables
  const { routes, bundle, locals, Head } = params;

  // Creating location object for the server router
  const location = new Location(req.path, req.query);

  // Running the router
  Router.run(routes, location, (error, initialState, transition) => {

    // If something went wrong, responding with 500
    if (error) return res.status(500).send(error);

    try {

      // Rendering <head> tag
      // Using `renderToStaticMarkup` here,
      // because we don't need React ids on these nodes
      locals.head = React.renderToStaticMarkup(
        <Head cssAsset={locals.cssAsset} />
      );

      // Rendering app
      locals.body = React.renderToString(
        <Router location={location} {...initialState} />
      );

      // Storing webpack chunks in variable
      // to expose it as global var in html for production bundles
      // It's related to long-term caching of assets (details below)
      const chunks = __DEV__ ? {} : require('public/assets/chunk-manifest.json');

      locals.chunks = serialize(chunks);

      // Defining path to jade layout
      const layout = `${process.cwd()}/app/bundles/${bundle}/layouts/Layout.jade`;
      
      // Compiling initial html
      const html = jade.compileFile(layout, { pretty: false })(locals);

      // 😽💨
      res.send(html);

    } catch (err) {

      // If something went wrong, responding with 500
      res.status(500).send(err.stack);

    }

  });

}

Jade template is pretty straightforward:

doctype html
html
  
  // Injecting `locals.head`
  != head

  body
    
    // Injecting `locals.body`
    #app!= body
    
    // Injecting `locals.chunks` global for webpack
    script.
      window.__CHUNKS__ = !{chunks};
    
    // Injecting js assets
    script(src="#{vendorAsset}")
    script(src="#{jsAsset}")

At this point initial html was rendered and sent to browser with client’s javascript app.

Client entry point

Client’s initter is much easier.

/* app/bundles/app/initters/client.jsx */

// Here we collect bundle specific stuff, same as for server

// Babel polyfill, required for some of the ES6/ES7 features
import polyfill   from 'babel/polyfill';

import initter    from 'app/libs/initters/client';
import routes     from '../routes/routes';


const params = { routes };

export default initter(params);
/* app/libs/initters/client.jsx */

// This initter is shared between all bundles

import React            from 'react';
import Router           from 'react-router';
import BrowserHistory   from 'react-router/lib/BrowserHistory';


export default (params) => {

  const { routes } = params;

  // Creating history object for the client router
  const history = new BrowserHistory();

  // Creating app container
  const AppContainer = (
    <Router history={history} children={routes} />
  );

  // Selecting DOM container for app
  const appDOMNode = document.getElementById('app');

  // Flushing application to DOM
  React.render(AppContainer, appDOMNode);

}

Next thing we need to learn is how to build the client bundles and how to setup production / development environments with gulp & webpack.

Gulp, Webpack & hot reloading

Building process is the biggest pain in the ass in javascript world, so get ready. We’re gonna setup two different environments: development & production. In the end there will be available 3 console commands:

  • npm start or gulp start:dev

    Runs development web server with hot reloading.

  • npm run prod or gulp start:prod

    Runs local server with production ready bundles to check how it works.

  • npm run build or just gulp

    Compiles production builds.

Key tools we’re gonna use are Gulp and Webpack. Here is the gulpfile:

/* gulpfile.babel.js */

import gulp         from 'gulp';
import webpack      from 'webpack';
import eslint       from 'eslint/lib/cli';
import run          from 'run-sequence';
import gutil        from 'gulp-util';
import { exec }     from 'child_process';
import del          from 'del';

import gulpConfig   from './build/gulp.config';


// Task name to compile production assets
const prodBuildTask = 'build';

// Task name to start dev server
const startDevTask = 'start:dev';

// Task name to start local server with production assets
const startProdTask = 'start:prod';

// Defining environment
const isDevBuild = process.argv.indexOf(startDevTask) !== -1;

// Defining default task
const startTask = isDevBuild ? startDevTask : prodBuildTask;

// Importing gulp config (it's function, returns an object)
const config = gulpConfig(isDevBuild);


/* Run tasks */

// Using `run-sequence` plugin to group [async tasks] in [sync groups of async tasks]
// 1 group: Cleaning public folder and linting scripts
// 2 group: Compiling assets and coping static stuff (fonts, favicon etc.) to public folder
// 3 group: Starting local Express servers

gulp.task('default', [startTask]);

gulp.task(prodBuildTask, done => {
  run(['clean', 'lint'], ['bundle', 'copy'], done);
});

gulp.task(startDevTask, done => {
  run(['clean', 'lint'], ['bundle', 'copy'], ['server'], done);
});

gulp.task(startProdTask, done => {
  run(['clean', 'lint'], ['bundle', 'copy'], ['server'], done);
});



/* Node servers starter */

// Helper to start Express servers from gulp
const startServer = (serverPath, done) => {

  // Defining production environment variable
  const prodFlag = !isDevBuild ? 'NODE_ENV=production' : '';
  
  // Starting the server
  const server = exec(`NODE_PATH=. ${prodFlag} node ${serverPath}`);

  // Handling messages from server
  server.stdout.on('data', data => {
    
    // Checking if it's a message from webpack dev server 
    // that initial compile is finished
    if (done && data === 'Webpack: Done!') {
      // Notifying gulp that assets are compiled, this task is done
      done();
    } else {
      // Just printing output from server to console
      gutil.log(data.trim());
    }
  });

  // If there is an error - printing output to console and doing the BEEP
  server.stderr.on('data', data => {
    gutil.log(gutil.colors.red(data.trim()));
    gutil.beep();
  });

};



/* Build bundles */

gulp.task('bundle', done => {

  if (isDevBuild) {

    // Starting webpack dev server
    startServer('server.dev.js', done);

  } else {

    // Just compiling assets
    webpack(config.webpack).run(done);

  }

});



/* Start express servers */

gulp.task('server', done => {

  const servers = config.server.paths;
  let queue     = servers.length;

  servers.forEach(server => {
    startServer(server);
    if (--queue === 0) done();
  });

});



/* Copy files to `public` */

gulp.task('copy', done => {

  const files = config.copy.files;
  let   queue = files.length;

  files.forEach(file => {
    const from = config.copy.from + file[0];
    const to   = config.copy.to + (file[1] || file[0]);
    exec(`cp -R ${from} ${to}`, err => {
      if (err) {
        gutil.log(gutil.colors.red(err));
        gutil.beep();
      }
      if (--queue === 0) done();
    });
  });

});



/* Lint scripts */

gulp.task('lint', done => {

  eslint.execute('--ext .js,.jsx .');
  done();

});



/* Clean up public before build */

gulp.task('clean', done => {

  del(['./public/**/*'], done);

});
/* build/gulp.config.js */

import webpackDevConfig   from './webpack.config.dev';
import webpackProdConfig  from './webpack.config.prod';


const _app    = './app';
const _public = './public';
const _assets = `${_app}/assets`;


export default (isDevBuild) => {

  return {

    webpack: isDevBuild ? webpackDevConfig : webpackProdConfig,

    server: {
      paths: ['./server.app.js']
    },

    copy: {
      from : _assets,
      files: [
        [ '/tinies/favicon.ico', '/' ],
        [ '/tinies/robots.txt',  '/' ]
      ],
      to: _public
    }

  };

}

For each environment there will be its own setup.

Production

First let’s define requirements for production build:

  • Asset name should contain identifier for long-term caching purposes. We should include hash of the asset file in URL, thus when we’ll update the app, visitor’s browser will download updated asset, rather than using old one from cache. More detailed explanation on subject — in this medium post.
  • Our app and vendor’s modules should be split in separated chunks. Every time we update the app, whole client’s asset is changed. So visitor have to re-download it, incl. vendor’s modules, that hasn’t been changed. If we will split client’s bundle in two chunks — app and vendor’s stuff — users won’t need to re-download vendor’s modules, because it’s in different chunk and vendor’s chunk is cached by the browser.
  • CSS asset should be extracted out of javascript bundle and placed in head tag above initial html. If we won’t do this, visitors will see un-styled content for a moment, because initial html from server is placed above the javascript assets. You will notice this in development mode.
  • Assets files should be minified & gzipped. So we will be serving smaller-sized assets to client.

For webpack compiler we have file with 2 end points:

/* build/bundles/app.js */

import scripts  from '../../app/bundles/app/initters/client.jsx';
import styles   from '../../app/bundles/app/layouts/Layout.styl';

Here is webpack production config:

/* build/webpack.config.prod.js */

import webpack        from 'webpack';
import Extract        from 'extract-text-webpack-plugin';
import Gzip           from 'compression-webpack-plugin';
import Manifest       from 'webpack-manifest-plugin';
import ChunkManifest  from 'chunk-manifest-webpack-plugin';
import path           from 'path';


export default {

  // Defining entry point
  entry: {
    
    // Bundle's entry points
    app: './build/bundles/app.js',
    
    // List of vendor's modules
    // To extract them to separate chunk
    vendor: ['react', 'react-router']
    
  },

  // Defining output params
  output: {
    path         : './public/assets',
    filename     : '[name]-[chunkhash].js',
    chunkFilename: '[name]-[chunkhash].js'
  },

  // Defining resolver params
  // On the server we use NODE_PATH=.
  // On the client we use resolve.alias
  resolve: {
    alias: {
      'app'   : path.join(process.cwd(), 'app'),
      'config': path.join(process.cwd(), 'config'),
      'public': path.join(process.cwd(), 'public')
    },
    extensions: ['', '.js', '.jsx']
  },

  devtool : false,
  debug   : false,
  progress: true,
  node    : {
    fs: 'empty'
  },

  plugins: [
    
    // Extracting css
    new Extract('[name]-[chunkhash].css'),
    
    // Extracting vendor libs to separate chunk
    new webpack.optimize.CommonsChunkPlugin({
      name     : 'vendor',
      chunks   : ['app'],
      filename : 'vendor-[chunkhash].js',
      minChunks: Infinity
    }),
    
    // Defining some globals
    new webpack.DefinePlugin({
      __CLIENT__   : true,
      __SERVER__   : false,
      __DEV__      : false,
      __DEVTOOLS__ : false,
      'process.env': {
        'NODE_ENV': JSON.stringify('production')
      }
    }),
    
    // Avoiding modules duplication
    new webpack.optimize.DedupePlugin(),
    
    // Extracting chunks filenames to json file
    // Required to resolve assets filenames with hashes on server
    // See `getAsset` helper in `app/libs/getAsset.js`
    new Manifest(),
    
    // Extracting chunks internal ids to json file
    // Required to keep vendor's chunkhash unchanged
    new ChunkManifest({
      filename        : 'chunk-manifest.json',
      manifestVariable: '__CHUNKS__'
    }),
    
    // Also required to keep vendor's chunkhash unchanged
    new webpack.optimize.OccurenceOrderPlugin(),
    
    // Minifying js assets
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        'warnings'     : false,
        'drop_debugger': true,
        'drop_console' : true,
        'pure_funcs'   : ['console.log']
      }
    }),
    
    // Gzipping assets
    new Gzip({
      asset    : '{file}.gz',
      algorithm: 'gzip',
      regExp   : /\.js$|\.css$/
    })
  ],

  // Defining loaders
  module: {
    noParse: /\.min\.js$/,
    loaders: [
      { test  : /\.jsx?$/, loader: 'babel?stage=0',  exclude: /node_modules/ },
      {
        test  : /\.styl$/,
        loader: Extract.extract('style', 'css!autoprefixer?{browsers:["last 2 version"], cascade:false}!stylus')
      },
      {
        test  : /\.css$/,
        loader: Extract.extract('style', 'css!autoprefixer?{browsers:["last 2 version"], cascade:false}')
      }
    ]
  }


}

npm run prod and we have compiled assets in _public/assets_ folder ready to deploy.

Development

Requirements for development build:

  • Local dev server should be hot reloadable. Webpage should automatically reflect all changes in js / css files on file save.

We don’t care about long-term caching, css extraction and assets minification in dev environment.

Hot reloadable dev server

The main idea of hot reloadable dev server, is that webpack is starting his own express server on some port (devPort in our config) and serving all assets from there with polling script on top:

<script src="[http://lvh.me:3001/assets/app.js](http://lvh.me:3001/assets/app.js)"></script>

Under the hood it observes the assets files, if it’s changed, it recompiles assets and sends changes to client. So you don’t need to reload page on every change. Here is how to set this up for React application:

/* build/webpack.config.dev.js */

import webpack      from 'webpack';
import path         from 'path';

import appConfig    from '../config/server.app';


export default {

  // Defining entry points for webpack dev server
  entry: {
    app: [
      `webpack-dev-server/client?http://lvh.me:${appConfig.devPort}`,
      'webpack/hot/only-dev-server',
      './build/bundles/app.js'
    ],
    vendor: ['react', 'react-router']
  },

  // Defining output for webpack dev server
  output: {
    path      : path.join(process.cwd(), 'public', 'assets'),
    filename  : '[name].js',
    publicPath: `http://lvh.me:${appConfig.devPort}/assets`
  },

  resolve: {
    alias: {
      'app'   : path.join(process.cwd(), 'app'),
      'config': path.join(process.cwd(), 'config'),
      'public': path.join(process.cwd(), 'public')
    },
    extensions: ['', '.js', '.jsx']
  },

  // Source maps are slow, eval is fast
  devtool : '#eval',
  debug   : true,
  progress: true,
  node    : {
    fs: 'empty'
  },

  plugins: [
    
    // Enabling dev server
    new webpack.HotModuleReplacementPlugin(),
    
    // Don't update if there was error
    new webpack.NoErrorsPlugin(),
    
    new webpack.optimize.CommonsChunkPlugin({
      name     : 'vendor',
      chunks   : ['app'],
      filename : 'vendor.js',
      minChunks: Infinity
    }),
    
    new webpack.DefinePlugin({
      __CLIENT__   : true,
      __SERVER__   : false,
      __DEV__      : true,
      __DEVTOOLS__ : true,
      'process.env': {
        'NODE_ENV': JSON.stringify('development')
      }
    })
  ],

  module: {
    noParse: /\.min\.js$/,
    loaders: [
      
      // Notice `react-hot` loader, required for React components hot reloading
      { test  : /\.jsx?$/, loaders: ['react-hot', 'babel?stage=0'],  exclude: /node_modules/ },
      {
        test  : /\.styl$/,
        loader: 'style!css!autoprefixer?{browsers:["last 2 version"], cascade:false}!stylus'
      },
      {
        test  : /\.css$/,
        loader: 'style!css!autoprefixer?{browsers:["last 2 version"], cascade:false}'
      }
    ]
  }


}

And here is webpack development server, which we start by npm start (before express server of our app):

/* server.dev.js */

require('babel/register')({
  extensions: ['.js', '.jsx'],
  stage     : 0
});

var webpack          = require('webpack');
var WebpackDevServer = require('webpack-dev-server');
var webpackConfig    = require('./build/webpack.config.dev');
var serverConfig     = require('./config/server');


var initialCompile = true;

var compiler = webpack(webpackConfig);

var devServer = new WebpackDevServer(compiler, {
  contentBase       : 'http://lvh.me:' + serverConfig.devPort,
  publicPath        : webpackConfig.output.publicPath,
  hot               : true,
  inline            : true,
  historyApiFallback: true,
  quiet             : false,
  noInfo            : false,
  lazy              : false,
  stats             : {
    colors  : true,
    hash    : false,
    version : false,
    chunks  : false,
    children: false
  }
});

devServer.listen(serverConfig.devPort, 'localhost', function(err) {
  if (err) console.error(err);
  console.log('=> 🔥  Webpack development server is running on port %s', serverConfig.devPort);
});

// This part will notify gulp that assets are compiled
// Gulp will start application Express server then
// (and it'll pick up assets filenames from json manifest to inject them in html)
compiler.plugin('done', function() {
  if (initialCompile) {
    initialCompile = false;
    process.stdout.write('Webpack: Done!');
  }
});

Now we can run npm start and open** http://lvh.me** in browser. Every time we make a changes in js or css files, page will be automatically updated by webpack dev server.

Conclusion

We’ve learned how to build simple universal javascript app using React (Dude! It’s not simple at all!). But this is only beginning. Real-world apps is much more complex, so here is the questions we have to answer next:

  • How to manage state in our app?
  • How to prefetch data on the server, so we can render html with data from API and serve it to client?
  • How to secure it?
  • How to deploy it to production server?

Stay tuned to get the answers!

Part I: Planning the application

Part II: Building JSON API

Part III: Building Universal app

Part IV: Making Universal Flux app

Closing Remark

Could your team use some help with topics like this and others covered by ShakaCode's blog and open source? We specialize in optimizing Rails applications, especially those with advanced JavaScript frontends, like React. We can also help you optimize your CI processes with lower costs and faster, more reliable tests. Scraping web data and lowering infrastructure costs are two other areas of specialization. Feel free to reach out to ShakaCode's CEO, Justin Gordon, at [email protected] or schedule an appointment to discuss how ShakaCode can help your project!
Are you looking for a software development partner who can
develop modern, high-performance web apps and sites?
See what we've doneArrow right