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 thepackage.json
shows how complicated it can quickly become. - Separate out
dev
andprod
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.