Rails + React + NPM: Another Option

In a previous blog post on using React with Rails, my colleague, Jason Roelofs, made the case for eschewing the react-rails gem and instead going with browserify-rails.

Jason’s argument was mainly about dependencies – the react-rails gem bundles up some JavaScript into a ruby gem and then has you //= require it in application.js. And if you want to use a third-party JavaScript module in your app, then you’re stuck with typical Rails avenues for doing that – vendor the source, or find a version that has been wrapped in a ruby gem so you can put it in your Gemfile and use bundler to manage it. Both of these options are pretty gross, and they’re also quite painful if you decide to go all-out with React and use some of many modules that are now available for the platform.

Fortunately, there’s a better way. Jason’s piece goes into detail on how to let Rails be Rails and let JavaScript be JavaScript, both in the same application, so that you can use npm to manage JavaScript dependencies and import third-party modules into your Rails app.

But as nice as it is to just let npm manage JavaScript dependencies, I really like the view helpers in react-rails, and as I worked on my toy app for converting uploaded Mint.com exports into QIF files (more on that in a later post), I realized that I wanted to be able to use those helpers while still bringing in npm modules the browserify-rails way.

Sadly, most of the instructions I found for combining react-rails with browserify-rails and npm modules were at least a year out-of-date. After an embarrassingly long time of struggling to get the two to play nicely (I’m a relative n00b to JavaScript), I finally got it all working. It turns out that it’s pretty straightforward.

In this bog post, I’ll show you how to use the convenient react-rails helpers to build views while using browserify-rails to bring in npm modules where and when you need them.

How It Works

The secret to getting react-rails and browserify-rails to work together is mostly about what you don’t do. Specifically, don’t follow the instructions in the react-rails readme for setting up your app to work with the gem. This means skipping the packaged installer and so on. Forget about that stuff for now, because we’ll have take a slightly different route to get to the same destination.

Note: The instructions below assume that you’ve followed all of the steps in Jason’s blog post to get your app set up to use browserify-rails with React. If you’re starting all of this afresh, then go ahead and do Jason’s steps first, then come back here.

Now that you’ve got browserify-rails all set up, go ahead and add react-rails to your Gemfile and bundle install. Now let’s configure the app.

Setting up application.js

Your application.js file should look as follows:


//= require_self
//= require react_ujs

window.$ = window.jQuery = global.$ = require('jquery');
var React = window.React = global.React = require('react');
var ReactDOM = window.ReactDOM = global.ReactDOM = require('react-dom');

require('./components');

Let’s step through what’s going on with the above lines.

The first thing you’ll notice is what’s missing: we’re not requiring React, because that module is in our package.json, so browserify-rails will automagically make it available to us. Indeed, the only things we’re requiring here are JavaScript modules that we can’t get npm packages for (in this case, there’s only one). For instance, I went ahead and did npm install jquery-ujs -~~save because there’s a version of jquery-ujs that has been extracted from a rails gem and is available via npm; so now I don’t have to require it in the application.js manifest. I also don’t need to require react-server for the same reason -~~ I just used npm for that.

The one JavaScript file I need to make react-rails work that isn’t available via npm is react_ujs, so I need to require it here. To understand the lines of code that follow this require, we need to know more about react_ujs and what it does.

The react_component helper method that react-rails gives you generates a div tag that populates data attributes with a React component’s name and props. After the DOM is loaded, the react_ujs module then goes back and puts the proper containers for the React components inside those divs.

But react_ujs doesn’t import React, ReactDOM, or JQuery itself; rather, it assumes that you’ve imported these and have defined them on window (just as react-server assumes you’ve defined them on global). If you’re just using react-rails for the handy helper features, as we are here, then you have to put these modules onto window manually so that react_ujs can find them and do its thing.

The final line requires components.js, which brings us to our next topic.

Configuring components.js

Because we didn’t run rails g react:install, we must manually create the app/assets/javascript/components/ directory and the app/assets/javascript/components.js file. We’ll still be using those just like the standard react-rails install instructions indicate – all of our components will go into the app/assets/javascript/components/ directory as specified in the react-rails docs, but the components.js file will have different contents:

 require( 'babel-polyfill' );

// Manually add components to window and global
// so that react_ujs and react-server can find them and render them.
window.FileUploadArea = global.FileUploadArea = require("./components/file_upload_area.es6.jsx").default

Just like we had to define React and other modules on window, we also have to define all of our React components there, as well. So for every component we put in our components directory, we have to manually define that component on window for react_ujs (and on global for react-server, if we’re doing server-side rendering).

In the code above, I’ve created a component called FileUploadArea that I want to make available to my Rails views, so to that end I’m defining this component on window and global.

I’ve also required babel-polyfill here so that I can use it in my components.

Speaking of components, let’s go ahead and take a look at my little FileUploadArea component, which is still a work in progress at the moment:


import React from 'react';
import ReactDOM from 'react-dom';
import Dropzone from 'react-dropzone';
import axios from 'axios';
import axiosDefaults from 'axios/lib/defaults'

class FileUploadArea extends React.Component {
  onDrop(files) {
    var authenticityToken = $('meta[name="csrf-token"]').attr('content');
    var instance = axios.create({
      timeout: 1000,
      headers: {'X-CSRF-Token': authenticityToken}
    });

    instance.post('/imports')
    .then((response) => {
      console.log(response);
    })
    .catch((response) => {
      console.log(response);
    });
  }

  render() {
    return (
      <div className="form-control">
        <Dropzone onDrop={this.onDrop}>
          <div>Drop files here</div>
        </Dropzone>
      </div>
    )
  }
}

export default FileUploadArea

I include this half-baked bit of code mainly to show that I can now use any npm package that I’ve added to package.json, and then I can add the resulting component to a Rails view as follows:


<%= react_component('FileUploadArea') %>

With that in my view, react_ujs will do its thing and turn the resulting div into a container that React can mount my FileUploadArea component onto.

Conclusions

What I’ve outlined above isn’t perfect, but it’s pretty decent. This will get me started with integrating React into a Rails app in a way that still feels “railsy” yet doesn’t force me to resort to awkward and painful methods of dealing with third-party JavaScript.

Note also that if I have JavaScript in some of the gems I’m using, I can still require those modules in the application.js manifest just like old times – I don’t technically have to use npm for everything. The old Rails way still works perfectly well alongside the new one.

Photo of Jon Stokes

Jon is a founder of Ars Technica, and a former Wired editor. When he’s not developing code for Collective Idea clients, he still keeps his foot in the content world via freelancing and the occasional op-ed.

Comments

  1. sylarruby@gmail.com
    Dave
    August 27, 2016 at 11:36 AM

    Error: Couldn’t find preset “es2015” relative to directory

  2. todd.groff@gmail.com
    Todd
    September 29, 2016 at 14:58 PM

    Hi Jon,

    Thanks for this post. I’ve been trying to replicate the setup you and Jason put forth between these two tutorials. I’m now running into trouble trying to do server-side rendering.

    Your tutorial recommends just using react-server via npm. I have installed it and have defined everything on `global` in addition to `window`, but still get the following:

    ‘“ReferenceError: CollectionsGroupContainer is not defined” when prerendering CollectionsGroupContainer with {…}’

    I’m a fairly green dev, and new to both npm and React, so is there something obvious that I could be missing?

  3. November 09, 2016 at 7:37 AM

    do you have a working exampe on git?

  4. nolandcarroll@gmail.com
    Nolan Carroll
    December 20, 2016 at 22:25 PM

    Just wanted to say that this post was super helpful. Tried to get this working for hours and your solution finally made things work. Thank you!

  5. joel.d.cornelius@gmail.com
    Joel Cornelius
    January 23, 2017 at 23:11 PM

    Following both Jason’s tutorial and then this one on an existing rails app simply does not work. I have not added any react components yet, just wanted to see if I could set this up and have it play nicely with my the assets pipeline. A simple render of my app’s main page results in:

    ActionView::Template::Error (Error while running /<APP_DIR>/node_modules/.bin/browserifyinc -t [ babelify --presets [ es2015 react ] ] --list --cachefile=/<APP_DIR>/tmp/cache/browserify-rails/browserifyinc-cache.json -o "/<APP_DIR>/tmp/cache/browserify-rails/output20170123-71015-11o88m6" -:

    Error: Cannot find module ‘babelify’ from ‘~/.rvm/gems/ruby-2.3.1/gems/react-rails-1.10.0/lib/assets/javascripts’

  6. joel.d.cornelius@gmail.com
    Joel Cornelius
    January 23, 2017 at 23:18 PM

    Hi Jon, thanks for writing this up. I’m having issues making this work with an existing rails app. Following the steps outlined in Jason’s blog post and then those listed here is simply not working. Navigating to my app’s main page results in error:

    ActionView::Template::Error (Error while running /node_modules/.bin/browserifyinc -t [ babelify --presets [ es2015 react ] ] --list --cachefile=/tmp/cache/browserify-rails/browserifyinc-cache.json -o "/tmp/cache/browserify-rails/output20170123-71015-11o88m6" -:

    Error: Cannot find module ‘babelify’ from ‘~/.rvm/gems/ruby-2.3.1/gems/react-rails-1.10.0/lib/assets/javascripts’

    I have installed all the necessary gems and npm modules, and my rails server starts up without issue. It’s the rendering that triggers this error

  7. Joel
    January 23, 2017 at 23:36 PM

    Wow, sorry for all those 3 identical posts! I solved the issue - my manifest file somehow got improperly formatted when I edited it for this tutorial.

  8. February 15, 2017 at 9:08 AM

    Jon, did you ever take a look at https://github.com/shakacode/react_on_rails?

  9. wlgriffiths@gmail.com
    Luke Griffiths
    August 27, 2017 at 18:47 PM

    Thanks for tackling this problem, Jon. I’m about to dive into this solution to see if I can get it to work.

    You said “The instructions below assume that you’ve followed all of the steps in Jason’s blog post to get your app set up to use browserify-rails with React.”, but there’s no link to this so it’s ambiguous what it refers to.

    Manually browsing the blog archives leads me to https://collectiveidea.com/blog/archives/2016/03/09/modern-javascript-and-rails, and I’m gonna assume that’s the post it refers to.

  10. wlgriffiths@gmail.com
    Luke Griffiths
    August 27, 2017 at 22:35 PM

    Never mind I’m an idiot. Link to Jason’s blog post is literally in the first sentence of your post. Disregard my message.

  11. rails
    January 19, 2018 at 12:45 PM

    this don’t work

  12. rails
    January 19, 2018 at 13:05 PM

    this don’t work

  13. Vineeth
    January 24, 2018 at 9:47 AM

    Thanks for the article. Just a question, we use Rails5 and BackboneJS. Can I start using ReactJS with it along with few npm module, since ReactJS has lot of modules dependant on npm. It’ll help a lot if you can highlight on some possibilities or challenges in doing the same.