🔥 A step-by-step React tutorial for Hot Module Replacement in Rails

Hrishi Mittal
HackerNoon.com
Published in
11 min readFeb 22, 2017

--

Don’t you hate refreshing your browser every time you change a bit of code?

This is a lesson from The Complete React on Rails Course, which helps Ruby on Rails developers become amazing at building UIs in Rails using React.js. It’s only meant for people who are serious about using React in production, so don’t click this link unless you’re one of them.

In this lesson, we’re going to look at how to use hot asset reloading in a react_on_rails app with Hot Module Replacement.

The app we’re building is a calendar appointments app. You can find the code on Github:

Hot reloading means automatically showing live code changes in the browser without having to reload the page every time you change something.

It’s quite useful in development. You can have your code and browser windows next to each other and simply write code and save it, and see the changes immediately without switching over to the browser or refreshing it.

The react_on_rails gem uses a special type of hot reloading called Hot Module Replacement or HMR via Webpack Dev server to provide the hot assets to Rails, rather than the asset pipeline.

When you start the app, Webpack builds the bundle and then continues to watch the source files for changes. If it detects a source file change, it rebuilds only the changed module(s) and updates them in the browser through an HMR runtime.

The react_on_rails gem has a README on how to set this up.

BUT the example code they provide has a lot of extra bits and it’s easy to get lost in npm hell trying to do all of that at once.

So I made this super simplified step-by-step tutorial on how to do it.🙂

I recommend you clone the react_on_rails repo anyway on your computer and look in the spec/dummy directory for the hot reloading setup code example.

You have to change a number of files and settings, so I’m going to take you through the setup step by step.

You can do hot reloading for any assets including Javascript, CSS and images but in this lesson we’re only going to use it for Javascript.

Hot reloading is only meant for development, so we need to produce two sets of config files — one for development to use hot reloading and one for production (to use static assets).

Let’s first do the setup for hot reloading and we’ll add the extra setup for serving static files in production, later.

1. We’re first going to install some javascript packages we’ll need.

The two key packages are react-transform-hmr (which enables reloading through a hot module replacement API) and webpack-dev-server (the development server that serves the live assets).

In addition, we need a babel plugin for applying the transform as well.

We need to add these to our client/package.json file:

“babel-plugin-react-transform”: “^2.0.2”,
“react-transform-hmr”: “^1.0.4”,
“webpack-dev-server”: “^1.16.2”

We’re also going to add jquery and jquery-ujs here so that they are bundled by webpack (instead of Rails) and made available to any other packages that depends on them.

“jquery”: “^3.1.1”,
“jquery-ujs”: “^1.2.2”,

Then let’s run:

$ npm install

And that’s the packages done.

2. Next, let’s set up the webpack config files.

We need a base config which will be shared by the hot config and the static one.

I’ve copied these config files from the react_on_rails repo spec/dummy/client directory into my app and simplified them to just use the basics that we need.

Let’s take a quick look at them.

The base config mainly sets up the entry points and file extensions to be resolved. It leaves the output setting for the environment-specific config files.

// Common client-side webpack configuration used by
// webpack.client.rails.hot.config and webpack.client.rails.build.config.
const webpack = require('webpack');
const path = require('path');
const devBuild = process.env.NODE_ENV !== 'production';
const nodeEnv = devBuild ? 'development' : 'production';
module.exports = {// the project dir
context: __dirname,
entry: [
'babel-polyfill',
'es5-shim/es5-shim',
'es5-shim/es5-sham',
'jquery-ujs',
'jquery',
'./app/bundles/Appointments/startup/registration',
],
resolve: {
extensions: ['', '.js', '.jsx'],
alias: {
libs: path.join(process.cwd(), 'app', 'libs'),
react: path.resolve('./node_modules/react'),
'react-dom': path.resolve('./node_modules/react-dom'),
},
},
plugins: [
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify(nodeEnv),
},
TRACE_TURBOLINKS: devBuild,
}),
],
module: {
loaders: [
{ test: require.resolve('jquery'), loader: 'expose?jQuery' },
{ test: require.resolve('jquery'), loader: 'expose?$' },
],
},
};

It also exposes jQuery as a global so that it’s available to any other packages dependent on it.

Now let’s look at the hot config. It builds on top of the base config.

It pulls in the basic config and adds some extra things for hot reloading. It sets the port where webpack dev server will run (3500).

// Run with Rails server like this:
// rails s
// cd client && babel-node server-rails-hot.js
// Note that Foreman (Procfile.dev) has also been configured to take care of this.
const path = require('path');
const webpack = require('webpack');
const config = require('./webpack.client.base.config');const hotRailsPort = process.env.HOT_RAILS_PORT || 3500;

It adds a couple of entry points — the first is the live asset from webpack dev server and the second is a module it needs.

config.entry.push(
`webpack-dev-server/client?http://localhost:${hotRailsPort}`,
'webpack/hot/only-dev-server'
);
Want this lesson on video? Click on the image above

Then it sets the output filename and path. We’re just calling it webpack-bundle.

config.output = {
filename: 'webpack-bundle.js',
path: path.join(__dirname, 'public'),
publicPath: `http://localhost:${hotRailsPort}/`,
};

If you look in the react_on_rails repo example, they split it into two files — one for the app and one for vendor files.

But we’re keeping it simple here and just creating one output file.

The most important bit here is this loader which takes our jsx files and applies the hmr transform through a babel plugin.

config.module.loaders.push(
{
test: /\.jsx?$/,
loader: 'babel',
exclude: /node_modules/,
query: {
plugins: [
[
'react-transform',
{
transforms: [
{
transform: 'react-transform-hmr',
imports: ['react'],
locals: ['module'],
},
],
},
],
],
},
},
{
test: require.resolve('jquery-ujs'),
loader: 'imports?jQuery=jquery',
}
);

And then we add the hmr plugin to the config and export it.

config.plugins.push(
new webpack.HotModuleReplacementPlugin(),
new webpack.NoErrorsPlugin()
);
config.devtool = 'eval-source-map';console.log('Webpack HOT dev build for Rails');module.exports = config;

3. The next step is to include the correct webpack assets file for each environment in our Rails application layout file, based on whether we’re using hot reloading or not.

We’ll use a view helper to configure the correct assets to load — either the “hot” assets or the “static” assets depending on the environment.

<!DOCTYPE html>
<html>
<head>
<title>Calreact</title>
<%= csrf_meta_tags %>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>

<%= env_javascript_include_tag(hot: ['http://localhost:3500/webpack-bundle.js']) %>
<%= env_javascript_include_tag(static: 'application_static',
hot: 'application_non_webpack',
'data-turbolinks-track' => true) %>
</head>
<body>
<%= yield %>
</body>
</html>

Here, env_javascript_include_tag is a helper method provided by react_on_rails which includes the hot or static asset file based on whether an environment variable is set to use hot reloading or not.

We have two tags here, one to load the hot assets which don’t use turbolinks and another for static assets.

And then we need to create these two files.

application_static.js:

//= require webpack-bundle
//= require application_non_webpack

We require the webpack-bundle first and then require application_non_webpack.

Let’s create that file now — application_non_webpack.js.erb:

<% if ENV["DISABLE_TURBOLINKS"].blank? %>
<% require_asset "turbolinks" %>
<% end %>

react_on_rails uses this bit of code to require turbolinks based on an env variable. We can use it to easily disable or enable turbolinks.

Ok so that’s all set up.

Now if we want to use static assets then we’ll include just the application_static file which includes webpack_bundle and any non webpack assets.

And if we’re using hot reloading then we’ll include the hot assets from webpack dev server and the non webpack assets like turbolinks from application_non_webpack.

4. Next we need to make sure that we include the webpack generated files in our assets initializer config file.

type = ENV["REACT_ON_RAILS_ENV"] == "HOT" ? "non_webpack" : "static"
Rails.application.config.assets.precompile +=
[
"application_#{type}.js"
]

So again here, based on this env variable we set the name of the file to include in our precompiled assets.

5. Next, we need to create a new Procfile for hot reloading.

We’ll call it Procfile.hot. Remember, it goes in the root of the app directory.

It has a couple of processes

# Procfile for development with hot reloading of JavaScript and CSS
rails: REACT_ON_RAILS_ENV=HOT rails s -b 0.0.0.0
# Run the hot reload server for client development
hot-assets: HOT_RAILS_PORT=3500 npm run hot-assets

First the rails server — it runs it with the REACT_ON_RAILS_ENV variable set to HOT.

Second, hot-assets which is a script for serving the hot assets on port 3500 via webpack dev server.

So that’s Procfile.hot all done.

6. We need to add the server-rails-hot.js script for running the webpack dev server to our client directory.

This file imports webpack, webpackdevserver and our hot config and then creates a new dev server and runs it on port 3500.

import webpack from 'webpack';
import WebpackDevServer from 'webpack-dev-server';
import webpackConfig from './webpack.client.rails.hot.config';const hotRailsPort = process.env.HOT_RAILS_PORT || 3500;const compiler = webpack(webpackConfig);const devServer = new WebpackDevServer(compiler, {
contentBase: `http://lvh.me:${hotRailsPort}`,
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(hotRailsPort, 'localhost', err => {
if (err) console.error(err);
console.log(
`=> 🔥 Webpack development server is running on port ${hotRailsPort}`
);
});

This file needs to be used by an npm script.
And that’s the last missing piece of the puzzle.

7. Let’s add some scripts in the two package.json files for npm to run our hot reloading code

First, let’s do the package in the root directory.

"scripts": {
"postinstall": "cd ./client && npm install",
"build:clean": "rm -r app/assets/webpack/* || true",
"build:dev:client": "(cd client && npm run build:dev:client --silent)",
"hot-assets": "(cd client && npm run hot-assets)"
}

The build:dev:client script is for static assets which we’ll look at in a minute.

But the hot-assets one is what we need for hot reloading.

It cd’s into the client directory and npm runs another script called hot-assets.

So let’s add that into the client package file now:

"scripts": {
"build:test": "npm run build:client && npm run build:server",
"build:client": "webpack --config webpack.client.rails.build.config.js",
"build:dev:client": "webpack -w --config webpack.client.rails.build.config.js",
"hot-assets": "babel-node server-rails-hot.js"
},

So here you can see the hot-assets script runs babel-node (a command line tool provided by babel) with our server-rails-hot.js file that we set up earlier.

And that’s everything we need to set up to get hot reloading of our javascript working.

So let’s see it in action now!

Let’s go to the terminal and fire up foreman with our new Procfile.hot:

$ foreman start -f Procfile.hot
foreman start -f Procfile.hot

You can see Webpack development server is running on port 3500

and our bundle gets compiled successfully.

Now let’s test our app in the browser.

To test hot reloading, I’m going to place my code editor next to the browser and you’ll see the live changes I make automatically appear in the browser.

Changes made to React components and utilities code appear immediately in the browser 🔥

If we look at the logs, we’ll see that webpack dev server is serving these hot updates.

Look at the size of the update files. They are a few bytes. So this is quite fast.

Alright, so that’s hot reloading set up for development.

8. Now one final thing is to set up the config for serving static assets to use in production or if we don’t want to use live reloading in development.

We need a couple of things — a static webpack config and a static Procfile.

Let’s add the webpack config first:

// Run like this:
// cd client && npm run build:client
// Note that Foreman (Procfile.dev) has also been configured to take care of this.
const webpack = require('webpack');const config = require('./webpack.client.base.config');const devBuild = process.env.NODE_ENV !== 'production';config.output = {
filename: 'webpack-bundle.js',
path: '../app/assets/webpack',
publicPath: '/assets/',
};
// See webpack.client.base.config for adding modules common to both the webpack dev server and railsconfig.module.loaders.push(
{
test: /\.jsx?$/,
loader: 'babel-loader',
exclude: /node_modules/,
},
{
test: require.resolve('react'),
loader: 'imports?shim=es5-shim/es5-shim&sham=es5-shim/es5-sham',
},
{
test: require.resolve('jquery-ujs'),
loader: 'imports?jQuery=jquery',
}
);
config.plugins.push(
new webpack.optimize.DedupePlugin()
);
if (devBuild) {
console.log('Webpack dev build for Rails');
config.devtool = 'eval-source-map';
} else {
console.log('Webpack production build for Rails');
}
module.exports = config;

I’ve used the webpack.client.rails.build.config.js file from the react_on_rails spec/dummy example and simplified it to only include the bits we need.

Similar to the hot webpack config, it builds upon the base config. It sets the output filename and adds the loaders we need.

Now let’s add a Procfile:

# Run Rails without hot reloading (static assets).
rails: REACT_ON_RAILS_ENV= rails s -b 0.0.0.0
# Build client assets, watching for changes.
rails-client-assets: sh -c 'npm run build:dev:client'
# Build server assets, watching for changes. Remove if not server rendering.
#rails-server-assets: sh -c 'npm run build:dev:server'

We need the top two processes, one for rails and the other for serving the assets.

The last process is for server rendering, which I’ve commented out.

The rails-client-assets script runs build:dev:client which we defined in package.json earlier like this:

"build:dev:client": "webpack -w --config webpack.client.rails.build.config.js",

It runs webpack with the build config file we just created.

Ok, so that’s the setup for production.

We can test it by running foreman with Procfile.static:

$ foreman start -f Procfile.static

We’ll no longer get the webpack dev server message in the logs and we’ll just have the rails app running on port 5000.

Now if we make a change in a component, it won’t appear automatically in the browser. We’ll need to reload the page manually to see the change.

So that’s hot reloading of javascript assets using Hot Module Replacement!

đź’š đź’š If you found this tutorial useful, please clickity click the green heart.đź’šđź’š

Oh and don’t forget to check out my course which has a full detailed video of this lesson and over 3 hours of more lessons which will take you from zero to hero using React with Rails.

This is a lesson from The Complete React on Rails Course, which helps Ruby on Rails developers become amazing at building UIs in Rails using React.js. It’s only meant for people who are serious about using React in production, so don’t click this link unless you’re one of them.

Hacker Noon is how hackers start their afternoons. We’re a part of the @AMI family. We are now accepting submissions and happy to discuss advertising & sponsorship opportunities.

If you enjoyed this story, we recommend reading our latest tech stories and trending tech stories. Until next time, don’t take the realities of the world for granted!

--

--