How to Use React with Rails19 Jan 2017
This post covers setting up with webpack 1.14.0, however the implementation doesn’t change much for webpack 2. There’s a follow up post here that covers those changes, though I recommend reading this one first.
First Things First; What Not to Do
A simple search for “react + rails” will turn up the react-rails gem immediately. It’s a first party gem you can drop into your rails app and get going quickly. It does a couple things:
It vendors React and a custom React UJS library for use with the Rails asset pipeline.
Adds a sprockets processor for handling JSX and ES6.
Adds a couple Rails helpers for rendering react components in our controllers and views.
With virtually 0 setup, you’re able to get started with React right away, and you can make awesome components like this:
Sssssshhhhhhhiiiiiiitttttt…how do I import npm libraries…? 🤔 Answer: you have to to use npm.
react-rails isn’t all bad, though. Remember that React UJS library I mentioned? Turns out that thing is pretty sweet. It lets us write views like this:
This works by rendering a
<div> with a bunch of data attributes that represents the component to mount and the props to pass to it. Then the React UJS library listens for the appropriate turbolinks events and
$(document).ready equivalents, and uses ReactDOM to render our component at that time.
The upside to this is we can easily mix our normal Rails views with our React components. The downside, though, is React UJS expects React, ReactDOM, and any components rendered with
react_component to be defined globally.
So What Should We Do?
So we know we need to use npm because there isn’t a gem for a good portion of libraries we may want to use. But how do we make that work with Rails?
We don’t want to ditch sprockets, or even gem bundled assets, completely. Rails ❤️ sprockets. The asset pipeline has great support for things like bootstrap, jquery, tubolinks, asset hashing–why throw all that away? So what we can do is use webpack to create a bundle of some of our assets, and pass that bundle to our asset pipeline.
As an example for how to do this, I built a version of the TodoMVC app using Rails, React, and Redux that you can see running here and the code here. We’ll use code samples from that application in what follows.
Get Your System Ready
Since we’re going to use node modules and webpack, we need to install the tool for that. I’m using yarn as opposed to npm, and it’s what I’d recommend. Anyway, installing node and yarn is pretty simple with homebrew:
brew install node brew install yarn
And now we need to use yarn to install webpack:
yarn global add firstname.lastname@example.org
And depending on your homebrew setup, you may need to adjust your
.bash_profile to add packages yarn installed globally to your path. To test if you have the problem run
webpack -h, if you get an error about webpack not being installed, you need to add this to your
Lastly, add our development and application dependencies with yarn:
yarn add --dev babel-core babel-loader babel-polyfill babel-preset-es2015 \ babel-preset-react babel-preset-stage-2 expose-loader script-loader yarn add react react-dom react-redux redux whatwg-fetch
That first line is basically a handful of libraries webpack needs. The second line are libraries our application actually uses.
The Directory Structure
As a reference, here’s a look at what my
The biggest difference is we’ve added a
root_components folder for use with React UJS (more on that later). We also have an
application_entry.js file for webpack, which we’ll look at later. And the bundle files webpack creates.
Pretty normal, but hey, what’s
react_ujs still doing there?! We’re still going to use it! In fact, we still install the
react-rails gem. This gives us access to the
render_component helper, and a bundled version of the
react_ujs library we can include in our
application.js. But we’re not using it to include React.
Webpack takes it’s direction from a combination of a config file, and then one or more entry files that config file specifies. Let’s take our config file in pieces (you can see the whole thing here). First, we require the packages we need and define some helper methods at the top:
Because I’m a rails guy through and through, I prefer snakecase for files and variables, and camelcase for classes. So the
camelize allows us to pass a string like
'todos_list' and get back
filename takes a path and gets just the filename without the extension. And
allFilesIn will get all the files in a folder, recursively. We’ll use these functions in the next section:
This gets all the components in our
root_components folder, and attaches a loader to them. In this case, the expose-loader.
Lastly, we’re ready to generate the config object webpack will use:
We add two entry files: one for the fetch polyfill, copied directly from their documentation, and one for our application. We specify where we want our bundled files outputted with the
output object. And then we define our loaders. This is just an array of objects that represent a loader. At a minimum they have a
test property which we use to match files to process, and a
loader property, which says which loader to use. So we take our
components list and concatenate it with the babel loader. This will run against any of our
.js files or
.jsx files under the
include path, or or
Last thing we need to do is define our entry file:
The first two lines require and globally expose React and ReactDOM. The last bit uses
require.context to require all of our
.jsx files in
/root_componets, which are exposed globally when they’re required because of the loader we setup in our config file.
Running it All
We can run webpack in development and watch modes with
webpack -d --watch --config webpack.config.js
But perhaps the best part, we’re able to do all that without giving up the things we love about Rails. Or turning our Rails app into strictly an API. We get to use Rails in the ways we love to; mostly serving server-renderd HTML and punting to React if and when it makes sense to. We still get all the gem-ified assets we’ve grown accostomed to using.