Webpack and Rails: the “should” knows

TL;DR: The webpacker gem makes webpack easy to use in a Ruby on Rails app. It bundles up page specific javascript packets and runs them through babel. After a few quick rails/rake commands from the command line, you’re off to the races.

https://giphy.com/explore/seabiscuit

In the beginning, there was rails. At first you were skeptical of the convention over configuration or you were just getting going with the web app stuff… Either way, you got used to it. No more php include’s or making sure all the script and style tags were there in the head — or better yet, just before the body tags. Rails just worked. You could focus on features and making use of all that ActiveRecord has to offer.

A while into this new hobby, you noticed everything was everywhere and your load time wasn’t quite as snappy as you’d want. You googled around the web and stumbled upon something like paloma. It was a little weird and controller specific but it got the job done or you settled: the ratio between better UX and a few extra kB was in balance. Plus turbolinks came out and made most clicks feel better.

At some point you started to dive deeper into new javascript: ES2015. After working your way through a half dozen blogs and videos preaching the true meaning of “this”, it started to feel better. Fat arrow functions made callbacks less confusing. Classes, modules and scopes all made things feel a bit more object oriented than the global javascript you started hacking with.

https://giphy.com/search/fight

Then you tried to bundle up your skills and make something happen in Rails. It went okay… I guess.

If you’re like me, this probably didn’t start out too well. You got browserify-rails working locally, only to find it broke uglifer when your app went to precompile in staging. Then you learned about uglifier and it’s unstable harmony. After wondering why you couldn’t just use “loose-envify” and why things are hard, you eventually learned what a polyfill is, got into the babel weeds and build something like this.

That’s all fine and dandy, but there is a better way!

https://giphy.com/search/code

Webpack via Webpacker to the rescue

The basic idea is this: bundle up the javascript you need and put it in an easy to use package without all the hassle or manual grunt/gulp work.

I won’t pretend to be the world’s authority on the matter. Looking here, here and here is probably the single best places to look if you want to dive in.

If you don’t, here are the need to knows:

  • When used from the command line, webpack takes two arguments: an entry point and an output location.
  • It starts at the entry point and recursively imports everything called for from top to bottom bringing what it reads into a single file, referred to as a bundle.
  • In plain English, it gets all the imported files from your modules and nearby paths into one place for importing and puts them into one transpired and polymorph’ed script tag.
import React from "react"; // add the things imported into that
import ReactDom from "react-dom"; // add the things from that
import { connect } from "react-redux"; // add the things from that
import * as api from "./api"; // add the things from that
import { action } from "./actions" // add the things from that
...// => bundle.js

This way instead of having a ton of <script src=”https://cdn.io/coolio”></script>’s in your view so you can access all the modules you want to use, you only need to include one (or two or three)!

Think of it like the sprockets asset pipeline, but based on the easily managed package.json file instead of through gems that give you //required items and spit-and-toothpick vendor/javascript files.

The exact process by which these bundles run through comes from a webpacker config. This config is highly customizable, which like everything is a bit of a headache when getting a grapple. To overcome this, the community has come out with things like `create-react-app` (think `rails new` of a React app) which give you a standard config to start with.

For us, this configuration comes from super easy set up and use webpacker gem and @rails/webpacker node_module. After reading through the docs, you’ll find that they touch the app in `config/webpack/${ENVIRONMENT}.js` and `bin/webpack`, respectively.

You can do as you wish, but I suggest you start by looking here, here and here. The first two are especially helpful in running the command line rake/rails tasks you need to get going.

In the end, you’ll end up with a new app/javascript directory that provides you magicly bundled javascript_ and stylesheet_ pack_tags named, by convention, as the same name you called the pack.

As your two widgets become three and four, you’ll likely wonder where to put things.

How to organize your Rails-React-Redux app

This is what I’ve found to be best:

/ app
/ javascript
/ components (React container and presentational components)
/ lib (utils like a CSRF Authenticity grabber)
/ modules (redux constants, actionCreators and reducers)
/ packs (where it all comes together)
/ stores (combinedReducers provided in the pack)

Under components…

I put my React container and presentational components grouped by “scene” or function group. Most often this is a Widget and the components that comprise it. As a guiding principle, I know when to break off into another group when the set of Api calls coming out of the component hits a different controller.

/ components
/ tasks
/ TaskWidget.js
/ TaskWidgetHeader.js
/ TaskFilter.js
/ TaskList.js
/ TaskItem.js
/ TaskCreationForm.js

Bonus: you can also add styles directly into your components that can be added to the DOM through stylesheet_pack_tags

// app/javascript/components/tasks/styles/task-widget.scss.task-widget {
display: flex;
...
}
// app/javascript/components/tasks/TaskWidget.jsimport React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import "./styles/task-widget.scss";
class TaskWidget extends Component {...}
...

Under lib…

Here I place reused util functions that cut down on boilerplate code throughout. This is one I’ve found particularly helpful for making sure Rails’ CSRF authenticity-token headers are brought along:

// app/javascript/lib/Authenticity.jsexport default {
token() {
const token = document.querySelector('meta[name="csrf-token"]');
if (token && token instanceof window.HTMLMetaElement) {
return token.content;
}
return null;
},
};
// used as such in my service api filesimport fetch from "cross-fetch";
import { checkStatus, parseJSON } from "../../lib/utils";
import Authenticity from "../../lib/Authenticity";
const baseURL = `${location.origin}/api`;export const updateThing = (thingId, params) => {
return fetch(`${baseURL}/things/${thingId}`, {
credentials: "same-origin",
method: "PATCH",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"X-CSRF-Token": Authenticity.token(),
},
body: JSON.stringify({ thing: params }),
})
.then(checkStatus)
.then(parseJSON);
};

Under modules…

I place all things to make a redux flux loop one place, grouped again by “scene” and the grouping of controller actions it interacts with for CRUD.

/ components
/ tasks
/ __tests__ (through Jest)
/ actions.js (redux actionCreators)
/ constants.js (export const ACTION = "ACTION" pairs)
/ reducer
/ index.js (combines the smaller parts for export)
/ byId.js
/ byFilter.js
/ service.js (the client-side of the api)

Under packs…

This is the entry point and where it all comes together! Having one per screen set is a good place to start. Eventually, the goal is likely to only have one that bundles up a single client-side app that makes use of a client-side router. Baby steps though…

/ app/javascript/packs/dashboard.js (landingPage === "dashboard")import React from "react";
import ReactDOM from "react-dom";
import dashboardStore from "../stores/dashboardStore";
import { Provider } from "react-redux";
import TaskWidget from "../components/tasks/TaskWidget";
document.addEventListener("DOMContentLoaded", function() {
const rootElement = document.getElementById("dashboard-task-widget");
if (rootElement !== null) {
const typeAndId = rootElement.dataset.typeAndId;
ReactDOM.render(
<Provider store={dashboardStore}>
<TaskWidget typeAndId={typeAndId} />
</Provider>,
rootElement
);
}
});

Under stores …

Pack-linked stores or sets of combined reducers that will define sensible state for the view at hand:

/ app/javascript/stores/dashboard.js (landingPage === "dashboard")import { createStore, combineReducers, applyMiddleware } from "redux";
import { composeWithDevTools } from "redux-devtools-extension";
import thunkMiddleware from "redux-thunk";
import throttle from "lodash/throttle";
import { loadState, saveState } from "../lib/localStorage";
import { reducer as formReducer } from "redux-form";
import clientCheckins from "../modules/clientCheckin/reducer";
import tasks from "../modules/task/reducer";
import notes from "../modules/note/reducer";
import { CREATE_TASK_SUCCESS } from "../modules/task/constants";const combinedReducers = combineReducers({
clientCheckins,
tasks,
notes,
form: formReducer.plugin({
TaskCreationForm: (state, action) => {
switch (action.type) {
case CREATE_TASK_SUCCESS:
return undefined; // <--- clear form data
default:
return state;
}
},
}),
});
const persistedState = loadState();const store = createStore(
combinedReducers,
persistedState,
composeWithDevTools(applyMiddleware(thunkMiddleware))
);
store.subscribe(
throttle(() => {
saveState({
tasks: {
filters: store.getState().tasks.filters,
},
});
}, 1000)
);
export default store;

Bringing it into the app for real!

// app/views/dashboard/index.html<div id="dashboard-task-widget"
data-type-and-id="All">
</div>
<%= javascript_pack_tag 'dashboard' %>
<%= stylesheet_pack_tag 'dashboard' %>

Hope this helps you get on your way.

Feel free to reach out to dan@gobloom.io if you need any pointers or are looking to work together on a project!