Tutorial - Setting Up a Simple Isomorphic React app

June 12, 2015

This tutorial is heavily deprecated. Please visit https://medium.com/front-end-developers/handcrafting-an-isomorphic-redux-application-with-love-40ada4468af4#.dqexya1fw for a much more up-to-date solution until I can get mine updated.

Note: this tutorial assumes React 0.13, React-Router 0.13, and Babel 5. Updates coming soon for the new versions

After hearing the responses of one of my other tutorials, it quickly became evident that a quick Google search on “setting up React” led to very confusing results. One of the biggest causes of this confusion is what an “isomorphic app” is. This is a big buzz word, and there are 100s of solutions on Github. I decided to take a different approach and create the simplest possible isomorphic application to help beginners get started, and understand what is going on.

The project is on Github, and the following is a quick walkthrough of how to start from scratch.

The goal of this project/tutorial to not only get someone’s projected started but to clarify the process behind an isomorphic app. (It should not be used in production without setting up several other things. Please see the Next Steps sections).

Isomorphism

The main idea behind an isomrophic app is that instead of bundling all your React code into a single bundle (and then using something like <script src="bundle.js"> in a bare bones html page), you run a Node.js server that serves the files for you. The main advantage to running the Node server is that you can render the React app server side, and simple display that rendered string to the user when they first visit the page. When the user visits the page, that rendered string gets overwritten after the bundle is downloaded and then the React app runs like normal.

This means a couple things:

  • A user can visit your site without having Javascript enabled (and at least have a usable version)
  • There isn’t an empty page while the user waits for the bundle to be downloaded (important for mobile usually)
  • You have the power of a Node backend for really anything (such as for data fetching)

With that, I will bring you through the entire set up process.

Tutorial

The tutorial will use the following stack:

Installation

Make sure that at least Node 0.12 is installed (check out nvm if you have questions on that).

In a clean directory, run npm init and fill out the questions it asks. The first real thing we will do is install all the necessary packages. Run the following command to do so:

$ npm install --save-dev babel babel-loader express jade react react-hot-loader react-router webpack webpack-dev-server nodemon

Afterwards create your directory structure:

$ mkdir src src/server src/shared src/client views

The reason we create server, shared, client directories is so that we can easily separate our concerns. The server folder will hold the backend Node server and Webpack dev server, the client entry point will render the React bundle, and the shared folder will hold your components, flux, routes, etc.

Additionally, add the following to the scripts in your package.json:

    "clean": "rm -rf lib",
    "watch-js": "./node_modules/.bin/babel src -d lib --experimental -w",
    "dev-server": "node lib/server/webpack",
    "server": "nodemon lib/server/server",
    "start": "npm run watch-js & npm run dev-server & npm run server",
    "build": "npm run clean && ./node_modules/.bin/babel src -d lib --experimental"

These are the build/watch tasks will be using, and will go over in more detail soon.

Jade view

In this tutorial, we elect to use Jade but feel free to use whatever templating engine you prefer. Using a template allows Node to render variables such as our server rendered React strings. Create a file called index.jade in the views folder, and paste the following:

html
  head
    title="React Isomorphic App"
    meta(charset='utf-8')
    meta(http-equiv='X-UA-Compatible', content='IE=edge')


    meta(name='description', content='')
    meta(name='viewport', content='width=device-width, initial-scale=1')

  body
    #app!= content

  script(src='http://localhost:8080/js/app.js', defer)

While all this may not make immediate sense, the key thing is the #app!= content line. In Jade, this will create an empty <div> with an id of app. Express will set the content of this div to content when it gets rendered on the server. Hence, this is where we tell Express to serve up the pre-rendered React. It is also the same <div id="app"> that we will tell the client to actually render the React bundle onto. So when the client finishes loading, we replace this rendered string in that div with our new bundle!

Lastly, the bottom script is just for development purposes only. This is where the webpack-dev-server will be running.

Webpack Dev Server

While in production you would not have the webpack dev server, in this project we have it continuously running for simplicity’s sake. Put casually, Webpack is what takes our all JS files and bundles it in such a way that the browser can load them. Having the dev server running will allow for live updates and automatic refresh (react-hot-loader!). This will run in parallel with the Node.js server that is actually rendering the app.

Create a new file called webpack.js in src/server/, and paste the following

import WebpackDevServer from "webpack-dev-server";
import webpack from "webpack";
import config from "../../webpack.config.dev";

var server = new WebpackDevServer(webpack(config), {
  // webpack-dev-server options
  publicPath: config.output.publicPath,
  hot: true,
  stats: { colors: true },
});

server.listen(8080, "localhost", function() {});

It very simply just create a new instance of a WebpackDevServer, and runs it on localhost:8080 in the background. The one key thing here is that it calls a config file from /webpack.config.dev.js.

Create webpack.config.dev.js in the root of your folder, and paste the following

var webpack = require('webpack');

module.exports = {
  devtool: 'inline-source-map',
  entry: [
    'webpack-dev-server/client?http://localhost:8080',
    'webpack/hot/only-dev-server',
    './src/client/entry',
  ],
  output: {
    path: __dirname + '/public/js/',
    filename: 'app.js',
    publicPath: 'http://localhost:8080/js/',
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoErrorsPlugin(),
  ],
  resolve: {
    extensions: ['', '.js']
  },
  module: {
    loaders: [
      { test: /\.jsx?$/, loaders: ['react-hot', 'babel-loader?experimental'], exclude: /node_modules/ }
    ]
  }
}

This is mostly boilerplate, but, starting at the top, we’re declaring an entry point for the webpack-dev-server to enter (the client entry point), then we specify an output for the bundle to be bundled to, all the necessary plugins for react-hot-loader, and then we declare our loaders. In this case, just babel so that we can write our ES6 Javascript.

React

Most boilerplates add a ton of complicated React to their boilerplates, but I find this just an over-complication. Creating components at this point is just like what you’d expect so we are just going to create a very simple called AppHandler. It is the entry point of all your components.

Create a new file AppHandler.js in src/shared/components, and paste the following

import React from "react";

export default class AppHandler extends React.Component {
  render() {
    return <div>Hello App Handler</div>;
  }
}

As you can see, this is rather unremarkable code and will just print out “Hello App Handler” in a div.

Node.js

It is now time to get our Node.js server spun up. Create a new file in src/server called server.js. Paste the following

import express from "express";
import React from "react";
import Router from "react-router";
const app = express();

// set up Jade
app.set('views', './views');
app.set('view engine', 'jade');

import routes from "../shared/routes";

app.get('/*', function (req, res) {
  Router.run(routes, req.url, Handler => {
    let content = React.renderToString(<Handler />);
    res.render('index', { content: content });
  });
});

var server = app.listen(3000, function () {
  var host = server.address().address;
  var port = server.address().port;

  console.log('Example app listening at http://%s:%s', host, port);
});

As you can see, the server is not too complicated. We first just import our packages and then construct an instance of express called app.

We next define our templating engine, Jade, and set the directory to views (we specify index soon). We then import our react-router routes (this will be done next section). And, after, we create a route handler for the root route (http://localhost:3000/).

This route handler is the most interesting point. Here, we use Router from react-router to take in that url (req.url) and render a string out of it based on our routes. This string is our react code at that route! We then just use Jade to render that content string to the variable content we defined in our index.jade. That’s it!

Lastly, we just spin up the server itself on port 3000 so that Node can take the requests.

Client

On the flipside, we handle the entry point for the client — src/client/entry.js. It is very simple, we essentially do that same thing from react-router but this time mount the React javascript onto the <div id="app"> from Jade. Here is the code:

import React from "react";
import Router from "react-router";
import routes from "../shared/routes";

Router.run(routes, Router.HistoryLocation, (Handler, state) => {
  React.render(<Handler />, document.getElementById('app'));
});

The key difference is instead of renderToString we use the full blown React.render method and give it the id of the div we want to mount on top of.

Routes

So the missing link that is combining our server and client are the react-router routes. If you have used react-router this will look nothing different to you. In /src/shared, create a new file called routes.js. Paste the following

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

import AppHandler from "./components/AppHandler";

export default (
  <Route handler={ AppHandler } path="/" />
);

All we simply do is import the AppHandler.js component we made earlier, set the path to / to display that component. So, at the root, we render that component.

So, consolidated, express takes a request at /. It goes to the routes, mounts react-router’s route at /, and then renders the string that created for the user. Meanwhile, the client is rendering and overwritting that server rendered responses by mounting on top of the <div id="app">.

Running the server

Go back to the root directory, and run npm start. This will run a watch task for Babel to handle the es6, webpack-dev-server to hot reloading, and the Node server.

Visit http://localhost:3000, and you should see “Hello App Handler”. If you change, the component the page should automatically refresh on save, as well (the dev server). Also, if you disable Javascript and reload, you will find that the component is still being rendered!

Next steps

These are pasted from the Github, but this is where to go now. This tutorial does not put you in a production ready state at all and there are many more considerations that you have to make as you set up your project:

  • Consider the best way to handle flux. There are many options that work in conjunction with the isomorphic server (namely flummox or fluxible). Both of these projects go into great detail about how to add their libraries into an isomorphic app.
  • Improve the build task system. Using npm scripts is definitely the simplest but a quick look at the package.json shows how complicated it can quickly become.
  • Separate out dev and prod environments using Jade and multiple webpack configuration files
  • Improve the hierarchy of the folders. server is a mess right now and should be cleaned up/separated into a structure that makes more sense and is easier to maintain.
  • Make the server rendering and client rendering asynchronous

--- Please feel free to contact me if you have any questions or if anything is unclear. Also, discuss on Hacker News.


Written by Joseph Furlott who lives and works in Brooklyn, New York. I am a software engineer that specializes in designing and building web applications using React. I work at Datadog as a software engineer.

© 2022
jmfurlott@gmail.com