Modern Javascript and Rails

Cleanly integrating modern Javascript into a Rails app

Updated November 27, 2016 with tweaks as well as an example Rails 5 site showcasing this setup.

Want to write ES6 and/or use JSX or any of a vast array of modern Javascript tools in your Rails app? What about also writing your Javascript tests with the same tools? It’s easy with Browserify-rails and Teaspoon!

I’ll explain my choices below, but I’d like to start this post with exactly what you need to get started.

# Gemfile

gem "browserify-rails"

group :development, :test do
    # Your choice of test library.
    # Also available, teaspoon-mocha / teaspoon-qunit
    gem "teaspoon-jasmine"

    # Teaspoon's front-end is written in CoffeeScript but it's not a dependency
    gem "coffee-script"
end
# config/application.rb

# Configure Browserify to use babelify to compile ES6
config.browserify_rails.commandline_options = "-t [ babelify --presets [ es2015 ] ]"

# Run on all javascript files
config.browserify_rails.force = true

# Alternatively, only run on .es6 files
# config.browserify_rails.force = ->(file) { File.extname(file) == ".es6" }

unless Rails.env.production?
    # Make sure Browserify is triggered when asked to serve javascript spec files
    config.browserify_rails.paths << lambda { |p|
        p.start_with?(Rails.root.join("spec/javascripts").to_s)
    }
end

Install the requisite npm packages locally (don’t commit node_modules to source control, just package.json).

npm install browserify browserify-incremental babelify babel-preset-es2015 --save

Install Teaspoon.

bundle exec rails g teaspoon:install

Write ES6 modules and tests!

// app/assets/javascripts/hello_world.js

class Hello {
    greet() {
        return "Hello world!";
    }
}

export default Hello;
// spec/javascripts/hello_world_spec.js

import Hello from "hello_world";

describe("Hello", () => {
    it("greets the user", () => {
        let hello = new Hello();
        expect(hello.greet()).toEqual("Hello world!");
    });
});

Run the tests with rake teaspoon or visit the web runner at localhost:3000/teaspoon!

I have also provided an example Rails site showcasing this functionality.

Why?

Writing Javascript for Rails apps has long been a workable experience, but in my opinion it has never been great. As Javascript is now moving forward at a blistering pace, the deficiencies and limitations of the current Rails setup are showing up more than ever. While Rails has been served decently well with the “javascript-in-a-gem” pattern (e.g. jquery-rails, or the coffee-script gem), I believe this pattern that has served its purpose but it is now time for it to retire. It’s time to let Javascript be Javascript. As we have bundler, Javascript has a fantastic package manager with npm, why don’t we just use it as well?

Specifically, the problems I have with the “javascript-in-a-gem” pattern are around dependency management. These gems add a level of indirection in two ways. First, these gems are often not maintained by the author of the javascript library in question, and thus can lag behind in updates. Second there is no guarantee that the gem includes all of the dependencies of the javascript library itself, which may require adding more gems. On top of this, these gems lengthen the Gemfile, increase the apps boot time, and further complicate upgrades.

So how do we hook up npm packages to the Rails Asset Pipeline? There are a number of options available to us now, including foregoing the Asset Pipeline entirely and building a custom pipeline with tools like webpack and brocolli, but given that Rails already has a pipeline, doing so adds a significant learning hurdle for any developer coming onto the project.

I recently started a new Rails app in which I wanted to use ES6 and React.js+JSX. After significant research and experimentation of many of the available options, I eventually settled on the browserify-rails project. This gem hooks directly into Sprockets, intercepting requests for Javascript files to pre-process through npm and babel via the the browserify library. It was by far the most seamless configuration and setup I found, working out of the box with just one configuration setting (as seen above).

With browserify-rails, adding and using npm packages is simple. Want to use react? Install and configure:

npm install react react-dom babel-preset-react --save
# config/application.rb

config.browserify_rails.commandline_options = "-t [ babelify --presets [ es2015 react ] ]"

With the front-end situated, I also wanted to be able to write javascript tests utilizing all of these same tools. I eventually settled on teaspoon which also hooks into the Rails asset pipeline. With another one-line configuration for browserify-rails (seen above) I had what I wanted. ES6 in the app and in tests, using the same tools. Win!

But what about…

react-rails or react-on-rails

While they are both great libraries that do exactly what you’d expect (and more), these fall under the same “javascript-in-a-gem” pattern that I feel needs to be retired. For react-on-rails, this library configures a webpack-based asset pipeline, thus supporting further extensions and library usage, but again adds a large cognitive load to add to a Rails app.

Sprockets 4 or sprockets-es6?

Rails 5 and its version of Sprockets is supposed to support ES6 compilation, but last I looked it does so through the babel-transpiler gem, which is not being actively maintained and may have significant issues upgrading to newer versions of babel. On top of this, there’s very little extensibility, forcing the use of “javascript-in-a-gem” or another asset pipeline anyway if you want to use other tools or libraries, making the entire setup a non-starter.

bower, grunt, gulp, brocolli, webpack, …

There are definitely valid reasons to go about building your own asset pipeline using these tools. There is a lot of power behind having full control over how assets are processed in your app, but if you’re like me and want to keep using the Rails Asset Pipeline, these tools are not trivial to use and add extra cognitive overhead both in understanding what’s going on and maintaining the stack over time.

I have seen simple setups using bower, configured to install packages into vendor/assets/javascripts, but not every library in npm is available in bower, and you’re adding another level of maintenance to the system. Similarly with rails-assets, which is simply bundling up bower packages in Ruby gems, you’re back in “javascript-in-a-gem” land, adding a dependency on bower and on another external service at https://rails-assets.org/.

With the research I’ve done, I do believe that browserify-rails and teaspoon are the best route forward for most Rails apps to make use of ES6, npm modules and babel with all of it’s plugins and transforms, integrating a full, modern javascript stack into any Rails app.

Further Notes

Polyfills

Babel doesn’t by default include a polyfill, so if you want to use new Javascript APIs like Object.assign, I recommend grabbing the babel-polyfill plugin and importing it first in your Javascript:

npm install babel-polyfill --save
// app/assets/javascripts/application.js

import "babel-polyfill";

Headless Teaspoon

Teaspoon uses phantomjs as the default test runner, but that may not work for some libraries. I’m not sure where the problem is, and will update if I find a fix, but my use of React and Draft.js is causing problems with headless test runs, so I am currently using the selenium-webdriver runner to run the tests in Firefox directly. If you’re using CI, you’ll need to configure your test runs to be able to open up Firefox (e.g. How to on Travis CI)

Travis CI caching

Running npm install on every test run can really slow down a test suite. Travis supports various types of caching and if you’re using Travis you’ve probably already got bundler caching enabled. I would recommend also caching the node_modules directory, but to properly cache both, you need to convert your configuration to a list of directories:

# .travis.yml

cache:
  directories:
  - vendor/bundle
  - node_modules

In Closing

Javascript has often and regularly been seen as a necessary evil of web development, but with the advent of ES6 syntax and the proliferation of really good tools and libraries, I have found myself quite enjoying Javascript development. Javascript isn’t going away, on the contrary it is quickly becoming a skill that almost all developers need to have to stay relevant. We use Rails because we love writing Ruby and using Rails. There’s no reason we can’t also enjoy Javascript as well.

Photo of Jason Roelofs

Jason is a senior developer who has worked in the front-end, back-end, and everything in between. He has a deep understanding of all things code and can craft solutions for any problem. Jason leads development of our hosted CMS, Harmony.

Comments

  1. oswluizf@gmail.com
    Oswaldo Ferreira
    September 26, 2016 at 19:00 PM

    Hey, thanks for the post!

    I’m having some trouble to use polyfill the same way you mentioned. I successfully import it on application.js but it does not include any new API on the file where it’s imported (application.js). I’m missing any build process on browserify?

  2. todd.groff@gmail.com
    Todd
    September 27, 2016 at 14:36 PM

    Hi Jason,
    Thanks for the post. I’m hoping to use this setup, but I’m having trouble getting the ES6 transpiling to work. I’ve tried to follow your instructions meticulously, but it keeps giving me ‘Unexpected token export(or import)’ syntax errors. Were there any steps that were assumed in the set up of browserify-rails that are necessary to get this functioning?

  3. September 29, 2016 at 10:14 AM

    @Oswaldo: Do you see babel-polyfill code at all in the resulting application.js file? I have not had any issues so far getting the polyfill to apply.

    @Todd: I double checked my setup with what’s in the post and I don’t see any differences. I assume the browser is throwing the “Unexpected token” error? Which would imply that the transpiling isn’t happening at all, or at least not on all files. Do you have output in your development log showing browserify compiling files when you request the page?

  4. todd.groff@gmail.com
    Todd
    September 29, 2016 at 10:28 AM

    Jason, thanks for responding.

    I’ve implemented a hack I saw listed (in this comment -> https://github.com/browserify-rails/browserify-rails/issues/19/#issuecomment-73743770) on a browserify-rails github issue for ES6 transforms. Narrows it down to the browserify_processor.rb file of the gem not adding checks for ES6 syntax.

    I’m just curious why you and others are not having that problem when it seems you’re using strictly ES6 module syntax.

    Now I’m having trouble trying to do server rendering for React using your colleague Jon’s guide (http://collectiveidea.com/blog/archives/2016/04/13/rails-react-npm-without-the-pain/), so here I go to comment on that post.

    Thanks again!

  5. September 29, 2016 at 12:02 PM

    @Todd: One thing that I may not have specified is that I don’t try to set a specific extension to my javascript files. All of my files are the regular “.js” extension and I just write everything in ES6. Not sure if that is relevant for your case. To hopefully help track down discrepancies, here’s the versions of various libraries I’m currently using:

    Ruby gems:

    • rails 5.0.0.1
    • browserify-rails 3.2.0

    NPM Packages:

    • babelify 7.2
    • babel-preset-es2015 6.5
    • browserify 13.0