How we Integrate React with Rails at Ivy to build a Powerful UX

Ivy is a dedicated business management tool that we built specifically for interior designers. We automate a lot of the complex accounting required to handle vendors, customers and payments, and allow designers to focus on the actual design process. And we want to give our users a really great client-side experience.

Doing Front-end Right

That’s why we decided to make client-side development a first-class citizen, despite our core software being built with Rails. While some React/Rails integration solutions can simply be dropped into Rails as a gem, we felt strongly about using a simple solution that allowed for flexibility, granular control, and complete front-end tooling. We wanted Rails to handle our back-end and npm & Webpack to build our front-end bundle.

The React/Rails Ecosystem

In researching existing libraries that integrate React into Rails, we came upon two gems that seem to be the market leaders: ReactOnRails and React-rails. ReactOnRails is the kitchen sink of React/Rails integration libraries and goes deep to offer a range of impressive features, including server rendered components. React-rails doesn’t really allow for a full front-end toolkit, so it was out for us, but otherwise it’s a nice, pared-down option.

We found that ReactOnRails was too much bulk to add to our existing web app. If we were starting a new web application from scratch, it may have been a good solution, and it certainly offers the most bells and whistles. But ultimately we chose to roll our own solution with some help from the wonderful Rails community that seems to have taken a shining to React.

Our integration does not have server-rendered components (yet), but it is lean, it uses Webpack, and if you’re using Heroku, it just works.

The secret sauce

Here’s the basic mechanism of the integration: We know that Rails reloads the entire application (including assets) on every page request in development mode, so we leverage this fact by having our Webpack-dev-server rebuild our bundle whenever the code changes and Rails pulls it in on each request. In production, we use Heroku’s NodeJS and Rails buildpacks to first run Webpack and then build the Rails app using the generated bundle. Ultimately React and Rails live alongside one another in relative harmony, with React components effortlessly utilizing CSS from the Rails application (or inline CSS).

Add a package.json file to your Rails app

Start by installing npm if you do not have it yet (npm -v should return a version number if it is installed).

Now initialize a new npm app in the root of your Rails app and install Webpack, Babel, Babel presets, React, and a networking library (to ensure compatibility between different browsers — I initially assumed that all browsers supported the new fetch API, and I was wrong):

> npm init
> npm install --save babel-loader babel-core babel-preset-es2015 babel-preset-react babel-preset-stage-0 react react-dom webpack isomorphic-fetch

Let’s go through all 9 of these dependencies so that we understand why each is necessary.

React is the npm packaged version of React and ReactDOM is responsible for mounting and umounting React components from the DOM.

Babel is the de facto and de jure choice for transpiling ES6 or ES7 code into a Javascript syntax supported by most modern browsers, and it allows us to make use of some terrific syntax improvements that later EcmaScript versions have ushered in. However, Babel 6 is no longer a single dependency — it has been split into smaller libraries. You’ll need babel-core as your starting point and babel-loader to integrate it into Webpack, and you’ll also need to add specific functionality in the form of plugins, as Babel 6 does not include any functionality by default. We add the following:

  • babel-preset-2015 — Adds ES6 support.
  • babel-preset-react — Adds JSX support and includes a number of smaller React-related plugins.
  • babel-preset-stage-0 — Adds support for certain ES7 syntax, such as class property initializers and async/await.

Normally you would save your coding dependencies as dev-dependencies. In this case, we want Heroku to use these dependencies when generating our React bundle file, and dev-dependencies are not loaded in production environments. So we’ll leave all of them as regular dependencies except for one — our webpack development server, which we will use to monitor our code for changes and rebuild our bundle file:

> npm install --save-dev webpack-dev-server

Directory Structure

We use a single directory called frontend that lives at the root of our app for all of our React components. The Rails directory structure is as follows:

/app
/config
/deb
/frontend
/featureOne
/components
/containers
/featureTwo
...
/utils
main.js

...

main.js is our entry file. It imports all the rest of our React module files as and hooks the React components onto the DOM. We look for a single DOM element, and use a React method to render the component onto the element:

// main.js
import React from 'react';
import ReactDOM from 'react-dom';
import ComponentOne from './ComponentOne/containers/ComponentOne';
document.addEventListener('DOMContentLoaded', function() {
let componentEntry = document.getElementById('replaceMeWithReactComponent');
if (componentEntry) {
ReactDOM.render(<ComponentOne />, document.getElementById('invoiceItemsTable'));
};
});

Now we’ll need a Webpack configuration file so that Webpack knows how to build the bundle:

// webpack.config.js
var path = require("path");
var webpack = require('webpack');
module.exports = {
context: __dirname,
entry: {
app: path.join(__dirname, 'frontend', 'main.js')
},
output: {
path: path.join(__dirname, 'app', 'assets', 'javascripts', 'react'),
filename: "[name]_bundle.js",
},
module: {
loaders: [
{
test: /\.js?$/,
exclude: /(node_modules)/,
loader: 'babel',
query: {
presets: ['react', 'es2015', 'stage-0'],
}
}
]
},
resolve: {
extensions: ['', '.js', '.jsx']
}
};

All .js files imported into main.js are transpiled by Babel and output into app/assets/javascripts/react/app_bundle.js. Simple enough.

// application.js
...
//= require react/app_bundle

Integrating with Heroku

Whereas before you used the Ruby buildpack alone, now you need to specify that the NodeJS buildpack is added and used before the Ruby one:

> heroku buildpacks:add --index 1 heroku/nodejs

# To view buildpacks: $ heroku buildpacks
# To remove node buildpack: heroku buildpacks:remove heroku/nodejs

For the magic to happen, your package.json file needs to tell Heroku what dependencies to install, and then to run Webpack as a postinstall script:

{
"name": "yourAppName",
"dependencies": {
"babel-core": "^6.10.4",
"babel-loader": "^6.2.4",
...
},
"devDependencies": {
"webpack-dev-server": "^1.14.1"
},
"scripts": {
"postinstall": "webpack --config webpack.config.js"
},
}

Now, whenever you push to a Heroku server, Heroku will first look for a package.json file, install all dependencies, generate a bundle using Webpack, and then resume your regularly-scheduled Rails app generation.

Setting up a Dev Environment

Once you have your React and Rails set up and integrated, you will need to be able to code in both environments. Before, we used `rails s` and `bundle exec sidekiq` to run our relevant processes. With React in the equation, we’ll also need to run Webpack. There are two options for this:

  1. Use the foreman gem to run multiple processes in a single terminal window. Then define a Procfile.dev file containing the relevant processes:
// Procfile.dev
webpack: webpack -wc --config webpack.config.js
web: bundle exec puma -C config/puma.rb
worker: bundle exec sidekiq -q default -q mailers -q quickbooks_sync

There is a clear downside to using foreman — whereas before you could drop in a binding.pry and interact with the server on debug, foreman can be buggy in displaying this interaction since there are multiple processes vying for the terminal’s input and output. There are solutions to this, such as remote debugging or a forked branch of foreman that is meant to have solved this, but I ended up just running each process separately. For good measure, I’m including instructions on the fix:

> gem uninstall foreman
> git clone -b readline-support https://github.com/kyrylo/foreman.git
> cd foreman
> gem build foreman.gemspec
> gem install foreman-0.77.0.gem

2. Or run each process in its own terminal window as before, adding in a window for the Webpack dev server. Just add a new window or tab and run:

> webpack -wc --config webpack.config.js

When you run webpack, you will see new JS bundles being created at app/assets/javascripts/react/app_bundle.js.

And that’s all there is too it. Thank you to Jack Callister, Hunter Husar, and my brilliant friends at Diacode (Javier Cuevas and Ricardo Vega), for helping us come up with a simple solution half a year ago.