Isomorphic React Dockerized
December 30, 2015
What is Isomorphic
It’s a fancy way of saying that the application is one where the client and server share components and rendering can be done on the server as well as on the client.
Why do it?
The reasons commonly cited for writing isomorphic apps are
Faster initial loads
Single page app frameworks tend to start by serving an empty html document and then fetching the content to display asynchronously. This can mean the user is looking at loading indicators or blank pages in the mean time. Sometimes, as a workaround, the server will pass along the first set of data, and the app will check for this data before making the calls.
With an isomorphic app, the initial request will handle fetching all of the data needed for display, and additionally will render the markup for that data and send the whole thing down (server side rendering).
Reusing code
The best aspect, in my opinion is getting to use the same libraries and components on the server as on the client. It is a very alluring prospect. This can ease maintenance and testing, make it easier to onboard to team members, and provide a more consistent development experience across the project.
SEO
This point is related to the first one. Having the full content rendered on the initial request means that web crawlers will be able to index your page appropriately.
Docker
Docker allows you to encapsulate your application with its dependencies. Docker is a large topic and will be the subject of other posts, so I won’t get into it right now but it is a great way to develop and deploy applications. In this demo we’ll use a dockerfile to build our application image.
The App
React is a library for rendering markup. One of the cool things that react can do is render markup on the server, just as well as it can on the client. The markup that is generated on the server is keyed with special markers that the client can use to pick up where the server left off.
As long as the client is rendering the same components, with same data it will be smart enough to not redraw the page, and instead will start up the library and hook into all of the events, as if it had been used from the start.
Let’s walk through the demo app that I’ve created at https://github.com/code-vicar/IsomorphicReactBase
The app is split into 3 pieces
client/index.js
This is the bootstrap file for the react app that will be given to the browser
import React from 'react'
import { render } from 'react-dom'
import { Router } from 'react-router'
import rootRoute from '../shared/routes/root'
import history from '../shared/services/history'
render(
<Router history={history} routes={rootRoute} />,
document.getElementById('app')
)
server/index.js
This is the express server that will be doing the server side rendering.
// handle react routes
app.get('*', (req, res, next) => {
// Note that req.url here should be the full URL path from
// the original request, including the query string.
match({ routes: rootRoute, location: req.url }, (error, redirectLocation, renderProps) => {
if (error) {
res.status(500).send(error.message)
} else if (redirectLocation) {
res.redirect(302, redirectLocation.pathname + redirectLocation.search)
} else if (renderProps) {
res.render('index', {
content: renderToString(<RoutingContext {...renderProps} />)
})
} else {
res.status(404).send('Not found')
}
})
})
It also serves the client javascript bundle. In production the bundle is served statically from the file system.
if (isProdEnv) {
// in production, serve the bundled client statically
app.use('/static', express.static(path.join(__dirname, '../client')))
}
In development the bundle is served from memory using webpack dev middleware and hot reloading middleware.
else {
// in development, serve the bundle dynamically with webpack hot reloader
// Step 1: Create & configure a webpack compiler
const webpack = require('webpack')
const webpackConfig = require(process.env.WEBPACK_CONFIG || '../../webpack.middleware.config')
const compiler = webpack(webpackConfig)
// Step 2: Attach the dev middleware to the compiler & the server
app.use(require('webpack-dev-middleware')(compiler, {
noInfo: true, publicPath: webpackConfig.output.publicPath
}))
// Step 3: Attach the hot middleware to the compiler & the server
app.use(require('webpack-hot-middleware')(compiler))
}
shared
This folder contains code that is not strictly intended for the client or the server, it is used in either context. For example, the root route
import React from 'react'
import history from '../services/history'
import App from '../components/App.js'
import About from '../components/About.js'
import Inbox from '../components/Inbox.js'
export default {
path: '/',
component: App,
childRoutes: [
{
path: "/about",
component: About
},
{
path: "/inbox",
component: Inbox
}
]
}
The other react components, and the history service
import { createHistory } from 'history'
import { createMemoryHistory } from 'history'
let history
if (typeof IS_CLIENT_BUNDLE !== 'undefined' && IS_CLIENT_BUNDLE) {
history = createHistory()
} else {
history = createMemoryHistory()
}
export default history
The history service is interesting because we’re using a webpack defined global to identify if we’re on the client or the server. The server cannot use the browser history that is the default for react router. When doing the server side rendering we must use memory history.
The express server has just a single view that is rendered for all requests. The result of the react server side rendering is passed to the view as content
<html>
<head>
<title>Isomorphic React Base</title>
<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' />
</head>
<body>
<div id="app">{{{content}}}</div>
<script src="static/index.js"></script>
</body>
</html>
This is a handlebar template, notice the triple bracket notation around the content, this means it will be rendered unescaped since we’re getting back an html string from the react-dom/server.
At this point you can run
$ npm install
$ npm run build
$ npm run start
and you’ll have an isomorphic react app with hot module reloading.
In your browser, navigate to the about page. Open your browsers development tools and watch the console. Edit the about component and add a letter to the text that is rendered. When you save the file you should see some messages show up in the javascript console in your browser indicating that it picked up the changes and used hot module reloading to update the webpack bundle. You should also notice that the text in the browser updated to match without needing a refresh.
So far so good, but what about docker?
The dockerfile describes how to build an image for running this react server
First it uses the official node base image
FROM node:5-slim
MAINTAINER Scott Vickers <[email protected]>
Then it downloads gosu, which will be used later to run the container as the correct user
ENV GOSU_DOWNLOAD_SHA256 6f9a6f5d75e25ba3b5ec690a5c601140c43929c3fe565cea2687cc284a8aacc8
RUN wget -O gosu -nv --ca-directory=/etc/ssl/certs "https://github.com/tianon/gosu/releases/download/1.5/gosu-amd64" \
&& echo "$GOSU_DOWNLOAD_SHA256 *gosu" | sha256sum -c - \
&& chmod +x gosu
Then it will create a directory to hold the react server. Before copying the full application in it will first start with the package.json file so that it can create a cache layer of the npm dependencies
# create a layer for dependencies so they're cached
RUN mkdir -p /isomorphic_react
WORKDIR /isomorphic_react
COPY package.json package.json
RUN npm install
Then the rest of the application is copied in
# copy the source and build
COPY . /isomorphic_react
RUN NODE_ENV=production npm run build
Here we create a user to run this container as
# add user and transfer ownership
RUN useradd -d /isomorphic_react scott \
&& chown -R scott:scott /isomorphic_react \
&& chmod -R g+rw /isomorphic_react
Choose the port that we’ll expose the server on and copy in the entrypoint.
EXPOSE 7777
COPY ./docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["npm", "start"]
The entrypoint script will always be the thing that executes whenever this container is run. It inspects the command that is passed to the container and if it is an npm command it runs it as the ‘scott’ user, otherwise it lets the command pass through and execute in the container environment.
#!/bin/bash
set -e
if [ "$1" = 'npm' ]; then
echo "Starting Isomorphic React Base"
exec /gosu scott "$@"
fi
echo "Passthrough command"
exec "$@"
Now that we have a dockerfile we can specify images to be built from it using docker-compose.
For each image that we want to describe we can create a docker compose file. For example, the docker-compose-dev.yml file
web:
build: .
ports:
- "7777:7777"
environment:
COMPOSE_PROJECT_NAME: isomorphic_react_dev
NODE_ENV: development
This compose file lets us say what port to map the container to in the host, as well as specify environment setting, and much more which we aren’t using in this demo app.
With this dockerfile and docker-compose file we have everything we need to ‘dockerize’ the server. If you have a docker host already, you could clone this repo to the host and run the docker compose file.
Alternatively, I’ve included a vagrantfile that will start and provision a VM with docker server and docker-compose installed. It also has provisioning scripts to build the images from the docker compose files.
# -*- mode: ruby -*-
# vi: set ft=ruby :
$bootstrap_docker = <<SCRIPT
bail() {
echo Error executing command, exiting
exit 1
}
exec_cmd() {
if ! [[ "$2" == "-q" ]]
then echo "Executing $1"
fi
sudo -n bash -c "$1" || bail
}
print_header() {
echo ""
echo "## $1 ##"
echo ""
}
print_banner() {
echo "############################################"
echo "## Install docker ##"
echo "## and docker-compose ##"
echo "############################################"
}
# main
print_banner
print_header "Add docker PPA"
exec_cmd "wget -qO- https://get.docker.io/gpg | apt-key add -"
exec_cmd "echo deb http://get.docker.io/ubuntu docker main | tee /etc/apt/sources.list.d/docker.list"
print_header "Install docker"
exec_cmd "apt-get -q update && apt-get -qy install lxc-docker"
print_header "Add vagrant user to docker group"
exec_cmd "usermod -aG docker vagrant"
print_header "Install docker-compose"
exec_cmd "curl -sL https://github.com/docker/compose/releases/download/1.5.2/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose"
exec_cmd "chmod +x /usr/local/bin/docker-compose"
SCRIPT
$build_dev = <<SCRIPT
mkdir -p /isomorphic_react_dev
cp -rf /vagrant/** /isomorphic_react_dev
cd /isomorphic_react_dev
docker-compose -f docker-compose-dev.yml build
docker-compose -f docker-compose-dev.yml stop
docker-compose -f docker-compose-dev.yml rm --force
docker-compose -f docker-compose-dev.yml up -d
SCRIPT
$build_prod = <<SCRIPT
mkdir -p /isomorphic_react_prod
cp -rf /vagrant/** /isomorphic_react_prod
cd /isomorphic_react_prod
docker-compose -f docker-compose-prod.yml build
docker-compose -f docker-compose-prod.yml stop
docker-compose -f docker-compose-prod.yml rm --force
docker-compose -f docker-compose-prod.yml up -d
SCRIPT
Vagrant.configure(2) do |config|
config.vm.box = "phusion/ubuntu-14.04-amd64"
config.vm.network "forwarded_port", guest: 7777, host: 8777
config.vm.network "forwarded_port", guest: 7999, host: 8999
config.vm.provision "bootstrap_docker", inline: $bootstrap_docker, type: "shell"
config.vm.provision "build_dev", inline: $build_dev, type: "shell"
config.vm.provision "build_prod", inline: $build_prod, type: "shell"
end
Now to get a dockerized isomorphic react app it’s as simple as
vagrant up
I hope this walkthrough has been helpful and inspires you to try out react and docker.
Setting up tests for react components and es6 code can be fairly complicated, so it wan’t covered here, but it is certainly doable, and encouraged.
Some gotchas to be careful of
React server side rendering is synchronous, so if you have asynchronous code running in the componentWillMount lifecycle event that updates the state it won’t work on the server. Also, the componentDidMount lifecycle event only gets executed on the client, not the server. https://github.com/facebook/react/issues/1739
The react transform hot module reloader does not work with the pure function syntax for defining react components. https://github.com/gaearon/babel-plugin-react-transform/issues/57
If your build requires authorization (such as private npm modules, private github, etc…) then you’re likely to run into problems while building the docker container. Build time secrets, an issue that is frustrating many docker users, is ongoing and you can follow it here https://github.com/docker/docker/issues/13490.
Feel free to leave questions in the comments below