When I began into my current job we had a small team but a lot of infrastructure. So we thought that could be a good idea move our stack to a more friendly plataform. In that time most of us was very familiar with Heroku, so it was our choice.

Looking to the decision now I have to say that was definetely the right thing to do. We had so much more free time to develop new things without have to dedicate too much on production setup. Recently we start to build some apps with a more JS oriented stack and the ideal scenario is have everything in one place. That was my motivation to learn what I will show to you here.

I know, you are so excited that your hands are shaking, you can't wait to show to your family, friends and cat how good your fresh React app is so we need to deploy. Just do it !

The basics

I will assume that you are already familiar with Heroku, but if you aren't, no problem. The first thing to do is install the Heroku Toolbelt, which is basically a library that will give you a command line integrated with git to access and interact with your app and Heroku. I recommend take a look at the friendly Heroku Docs to learn the basics before proceed and create the app with git following the Heroku intructions.

Dependencies

First let me explain to you something important: We have two ways of make a deploy: we can generate the bundle locally and send the file already packed to Heroku or we can run it in our production environment. I will show you how to do it using the first approach.

The reason why I'm telling you this now is because NPM have the dependencies and devDependencies. The dependencies are the packages that you will run in your production and development environment, devDependencies are only to your development environment aka your machine.

"dependencies": {
  "express": "^4.13.4",
  "react": "^0.14.8",
  "react-dom": "^0.14.8"
},
"devDependencies": {
  "babel": "^6.5.2",
  "babel-cli": "^6.6.5",
  "babel-loader": "^6.2.4",
  "babel-preset-es2015": "^6.6.0",
  "babel-preset-react": "^6.5.0",
  "babel-preset-react-hmre": "^1.1.1",
  "babel-preset-stage-0": "^6.5.0",
  "css-loader": "^0.23.1",
  "file-loader": "^0.8.5",
  "node-sass": "^3.4.2",
  "sass-loader": "^3.2.0",
  "style-loader": "^0.13.1",
  "url-loader": "^0.5.6",
  "webpack": "^1.12.14",
  "webpack-dev-middleware": "^1.6.1",
  "webpack-hot-middleware": "^2.10.0"
}

Webpack

This time we will create two webpack configuration files, one to production and another to development. Let's start with the prodution. We will create a webpack.prod.config.js file in the root of our project. It will look like this:

const path = require('path')
const webpack = require('webpack')

module.exports = {
  devtool: 'source-map',

  entry: [
    './src/index'
  ],

  output: {
    path: path.join(__dirname, 'public'),
    filename: 'bundle.js',
    publicPath: '/public/'
  },

  plugins: [
    new webpack.optimize.DedupePlugin(),
    new webpack.optimize.UglifyJsPlugin({
      minimize: true,
      compress: {
        warnings: false
      }
    }),
    new webpack.DefinePlugin({
      'process.env': {
        'NODE_ENV': JSON.stringify('production')
      }
    })
  ],

  module: {
    loaders: [
      { test: /\.js?$/,
        loader: 'babel',
        exclude: /node_modules/ },
      { test: /\.scss?$/,
        loader: 'style!css!sass',
        include: path.join(__dirname, 'src', 'styles') },
      { test: /\.png$/,
        loader: 'file' },
      { test: /\.(ttf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/,
        loader: 'file'}
    ]
  }
}

Breaking down the important parts:

devtool: 'source-map'

Normally in the development environment we use the eval option as devtool. If you wanna know more about it, take a look at this issue on Github. If you don't know what is a source-map read this doc right here.

plugins: [
  new webpack.optimize.DedupePlugin(),
  new webpack.optimize.UglifyJsPlugin({
    minimize: true,
    compress: {
      warnings: false
    }
  })
]
  • DedupePlugin - The DedupePlugin will search for equal or similar files and deduplicate them in the output. Warning, as the webpack doc says it's a work in progress and it can crash sometimes, but we are bleeding edge here guys, let's go ! Or not, it's your choice. :(

  • UglifyJsPlugin - You probably guessed, it will uglify and compress our code on production. You can read more about the project on the Github repo.

output: {
  path: path.join(__dirname, 'public'),
  filename: 'bundle.js',
  publicPath: '/public/'
}

In the snippet above we are setting the name of our bundle file, which will be the single file uglified, minified and optimized of our app generated by the Webpack. The public path of our files is in this case the public folder. Since we don't have this folder yet let's create a folder named public in the root of our project.

entry: [
  './src/index'
]

The ./src/index.js is our file where the main React component will be rendered. Let's create the ./src/entry.js:

import React from 'react'
import { render } from 'react-dom'
import App from './components/app'
import './styles/app.scss'

render(<App/>, document.getElementById('main'))

This file is pretty straight forward. Let's render an React component. It can contain absolutely anything, in fact you can render just a simple string here if you want instead of a React component.

To finish our production part let's add a index.html in the /public directory to load our generated bundle.js:

<html>
  <head>
    <meta charset="utf-8">
  </head>
  <body>
    <div id="main"></div>
    <script type="text/javascript" src="bundle.js"></script>
  </body>
</html>

To our development we will create a file named webpack.dev.config.js in the project's root:

const path = require('path')
const webpack = require('webpack')

module.exports = {
  devtool: 'eval',

  entry: [
    'webpack-hot-middleware/client',
    './src/index'
  ],

  output: {
    path: path.join(__dirname, 'public'),
    filename: 'bundle.js',
    publicPath: '/public/'
  },

  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoErrorsPlugin()
  ],

  module: {
    loaders: [
      { test: /\.js?$/,
        loader: 'babel',
        exclude: path.join(__dirname, 'node_modules') },
      { test: /\.scss?$/,
        loader: 'style!css!sass',
        include: path.join(__dirname, 'src', 'styles') },
      { test: /\.png$/,
        loader: 'file' },
      { test: /\.(ttf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/,
        loader: 'file'}
    ]
  }
}

Our development configuration will be simple. The only differences from productions are our devtool and plugins. The rest of the configuration are the setup was convered in the last post, so I will not get too much in depth here.

Server

It's time to our server configuration. Add a file named server.js in the src directory. Here we will just export our express server, it has just aa static path to the public directory (the location of our bundle) and the main path with our index.html:

const path = require('path')
const express = require('express')

module.exports = {
  app: function () {
    const app = express()
    const indexPath = path.join(__dirname, '/../index.html')
    const publicPath = express.static(path.join(__dirname, '../public'))

    app.use('/public', publicPath)
    app.get('/', function (_, res) { res.sendFile(indexPath) })

    return app
  }
}

Our file app.js with import the server module and plugin the webpack-dev-middleware and webpack-hot-middleware. The webpack-dev-middleware serves the files emitted from webpack over a connect server and webpack-hot-middleware will allow us to hot reload on Express.

const Server = require('./server.js')
const port = (process.env.PORT || 8080)
const app = Server.app()

if (process.env.NODE_ENV !== 'production') {
  const webpack = require('webpack')
  const webpackDevMiddleware = require('webpack-dev-middleware')
  const webpackHotMiddleware = require('webpack-hot-middleware')
  const config = require('../webpack.dev.config.js')
  const compiler = webpack(config)

  app.use(webpackHotMiddleware(compiler))
  app.use(webpackDevMiddleware(compiler, {
    noInfo: true,
    publicPath: config.output.publicPath
  }))
}

app.listen(port)
console.log(`Listening at http://localhost:${port}`)

Now, the webpack-dev-server will need a initial file index.html to read our application. Let's add it in the root directory:

<html>
  <head>
    <meta charset="utf-8">
  </head>
  <body>
    <div id="main"></div>
    <script type="text/javascript" src="/public/bundle.js"></script>
  </body>
</html>

* We are pointing to the public path in this case.

* If you don't want to repeat yourself with two similar html files there is the Html Webpack Plugin. This case is very simple, so I think it's not necessary.

Babel configuration

After the version 6 the Babel configuration is made with presets, which are modules that you can plug in and load to your app. It sounds complicated and one extra step at first but believe me, you will find it better. Add a file called .babelrc in the root directory, this file will load the presets that we included in our package.json. If you noticed, there is a key specifying development under env in the file, this is to load the presets listed inside it only on dev environment.

What we need to achieve: The react-hmre preset can't run on production.

hmr: hot module replacement

{
  "presets": [
    "es2015",
    "react",
    "stage-0"
  ],
  "env": {
    "development": {
      "presets": [
        "react-hmre"
      ]
    }
  }
}

Base component

If we are going to deploy a React app we need a component right ? Let's create a really basic component on the /src/components/ directory named app.js:

import React from 'react'

export default class App extends React.Component {
  render () {
    return (
      <div>
        <h1>Change me</h1>
      </div>
    )
  }
}

And add a SASS file on src/style named app.scss:

h1 {
  color: blue;
}

Running

We add two scripts at our package.json:

"scripts": {
  "build": "NODE_ENV=production webpack --config ./webpack.prod.config.js --progress --colors",
  "start": "node ./src/app.js"
}

Everything is ready. Now let's run our build:

$ cd myapp
$ npm run build

And our bundle.js will be generated on the public directory. Commit your changes and push to heroku with:

$ git push heroku master

You are done ! This tutorial is breaking the most important parts to configure your deploy, but if you got lost or just want to use the kit, go to the Github repository. Bye !