Reacting to Change
If you sit quietly enough while reading this article you’ll hear a sound. It’s the faint tapping of fingers on a keyboard, off in the distance in parts unknown. Those self-assured fingers are putting together yet another think piece on JavaScript in this—the year two thousand and seventeen—and how it makes the engineer writing it feel. You’ve read those articles. You’ve commented on them on Hacker News. You might have even ranted about them on Twitter.
This is not one of those articles.
Instead, this is the story of how we revitalized our frontend with a selection of new tooling and frameworks presented without comment on the pros and cons of other tooling and frameworks. I’ll actually be discussing how React is probably the greatest piece of software ever written and Dan Abramov should be proclaimed supreme overlord of JavaScr… oh there you go typing furiously in the comments section. Ah, right, I promised there would be no opinions on frameworks or tooling. Well, that’s a bit of a polite fiction. It’s impossible to avoid sharing opinions these days when new and interesting JavaScript libraries are popping up left and right. But the standard caveat applies: the opinions expressed here are the ones that have worked for us in our specific use case. If you find yourself in a similar situation, maybe you’ll give them a try also, but I’m not here to prescribe them for you—I’m not actually legally allowed to prescribe anything. With that in mind let’s get started.
In the summer of 2015, our frontend engineers sat down and tried to envision what our ideal frontend development environment would look like. Did we want to keep using Angular, as we had been the last couple years? Was it time to evaluate our build tools? How much did we want hot reloading, and why did we want it so very, very much? There were a lot of questions, and you don’t need another JavaScript fatigue article to tell you there are also a lot of answers to those questions. These are some of the decisions we made.
First, we decided to transition from Angular to React as our framework of choice. We considered Angular 2, but we didn’t think it was mature enough at the time for us to want to plot an upgrade path. React, on the other hand, had a growing user base of noteworthy software shops heavily invested in its success along with interesting solutions for handling application state in projects very similar to ours. As an added bonus, a handful of our developers were already using it in personal projects and were excited to start using it at work.
Next, we concluded that the time had come to decouple as much of our frontend from Rails as possible, with our ideal world being that Rails would only serve as a REST API for our JavaScript to interact with. Microservices are all the rage these days, but the main reason for doing this was to get our frontend assets off the Rails pipeline. Part of the reasoning for this was that our engineers really wanted to use webpack as our build tool. Webpack, in addition to being popular in the community, offers a variety of things such as hot module reloading, a development server, and build performance tools. Separating our build pipeline from Rails also offered a way for UX focused engineers to work completely independent of Rails, developing visual components that would be used in our application before implementing them as features.
The first thing we would tackle would be getting off the Rails asset pipeline in favor of webpack building all frontend assets.
Going Off the Rails
The world when this effort began:
About a year and a half ago all of our dashboard assets (JavaScript, CoffeeScript, and SASS) were built by Rails, using Sprockets. Rails managed all asset compilation, minification, and cache busting, as well as placing the assets in a CDN. The plan was to slowly offload that work to webpack incrementally, as we had a dozen or so engineers working on our application and we wanted to interrupt them as minimally as possible.
We allowed engineers to adopt new changes when it was convenient for them but also letting them know that their current workflow would eventually be deprecated. As a result, each step of the plan took place with opt-in functionality.
The first things to move off the pipeline were our SASS builds, with the responsibility of building our SASS falling to Gulp and node-sass, with browsersync to serve the compiled assets in development. Nothing changed in production, but this shift unlocked hot SASS reloading for our developers. As our monolithic rails app grew and load times for some analytics heavy pages increased, the hot reloading was a welcome addition for our developers to increase velocity on the frontend. While both Gulp and browsersync would not be a part of our build pipeline for long, they are noteworthy because they provided one of the first instances of hot reloading in our application and enabled UX focused engineers to work faster.
After successfully using CSS builds as a guinea pig for the project, it was time to start moving away from CoffeeScript as the language of choice. The first step was to enable developers to start using ES6, and we did so by using the Babel gem to continue using the rails pipeline temporarily. Enabling our developers to write ES6 made the transition away from CoffeeScript easier as they slowly acclimated themselves to new JavaScript language features (as well as exposing some of the deficiencies of CoffeeScript). Developers were still allowed to continue working with CoffeeScript, but all new features were encouraged to be written in ES6 so that the deprecation of CoffeeScript could begin.
Next, it was time to move CoffeeScript and JavaScript compilation off Sprockets for good. This was accomplished by slowly refactoring all of our existing code to CommonJS modules, moving the files away from the Rails app into its own separate “client” directory, and using Webpack to compile everything in that directory. Rails had previously handled javascript modules (as you can see in an old post of ours about a year on angular), but now CommonJS gave us a javascript only solution for modules and dependencies so that we could start using a javascript build system. Finally, we started using webpack to do our SASS builds also, and with the final CoffeeScript files moved to CommonJS our refactor to webpack was complete. All frontend assets are now built by webpack in both development and production.
Our end state:
Introducing React Alongside Angular
With our build tooling now set up for ES6 compilation, the time had come to begin using React. The path forward here, though, required reconciling our large Angular codebase with a brand new framework while Angular directives, controllers, and services were slowly phased out. Angular would need to continue to live (and still does) at the top level of our application, loaded on the page by Rails and setting up all the existing features and functionality.
In order to use React inside of Angular, we would end up writing a shockingly simple wrapper that turns a React component into an Angular directive. This way, we could have Angular render the directive, which in turn would start the process of rendering React.
module.exports = (AppContainer) => {
return () => {
return {
scope: true,
template: '<div></div>',
link(scope, element) {
ReactDOM.render(
<AppContainer />,
element[0]
);
scope.$on('$destroy', () => {
ReactDOM.unmountComponentAtNode(element[0]);
});
}
};
};
};
This very simple angular directive serves one purpose: taking an AppContainer and rendering it on the page at the div in the template. When the directive is destroyed, similarly, the React component should also be unmounted. You can also see that this is a Common JS module, so a separate file handles turning this into an Angular module so that Angular can find and then use the directive.
Since we could now put any arbitrary container, or smart component, on the page, we had unlocked the ability to start putting as much React as we wanted on Angular pages with the root being the Angular directive. This allowed us to rewrite entire pages from the ground up, first starting with replacing features individually before combining them under one container at the top level of the page. There is still quite a bit of Angular in our codebase, but we’re able to work around this by writing all new features in React and slowly replacing Angular as we go without being forced to do a complete rewrite.
Application State: Redux
With React in the codebase, it was time to assess how we would be handling application state as we moved away from Angular. We landed on Redux as the framework of choice, and set up the store for our application state. All smart components would share the same store, and these smart components could be placed anywhere on the page either with directives or nested below fellow react components (judiciously).
As part of our effort to divorce Rails and JavaScript, we began exposing our Rails models with an API for our asynchronous Redux actions to fetch data from. We decided that Rails models would form a 1:1 relationship with their counterpart in the Redux store, and wrote reducers accordingly. Previously with Angular, we were using the RailsResource library to handle all Rails model interactions. Now, all rails models are fetched, updated, deleted, created, etc. with simple AJAX requests using the fetch polyfill.
Many of the problems with our existing Angular code was the use of controllers and scope led to application state that was difficult to reason about. With Redux becoming our state container of choice, these difficulties evaporated and caused us to actively think about application state in ways we had not before. In particular, when working on new features redux and stateless components caused us to proactively consider different loading, error, and blank states for features in our application.
Tooling and Widgetpedia
React with Redux opened the door for us to start using developer tools in Chrome to both develop components and reason about application state. The react developer tools allow us to easily inspect the DOM created by React to find components and their respective properties for both debugging problems and implementing new features. Similarly, the redux developer tools have greatly helped squashing bugs, as following the state changes in successive actions makes for a great workflow.
Shortly after introducing React, work began on what we dubbed “Widgetpedia,” or a common React components library for internal use on our application. Widgetpedia not only functions for us as a library of reusable components, but also provides an environment for rapid prototyping of new ones. With no dependencies on our Rails APIs to load data, Widgetpedia is a place to do pure frontend development and iterate with a UX focus. The addition of hot reloading for our React components allowed us to create new ones quickly and easily before using them in features in our application.
Going Forward
It took quite a while to come up with a conclusion to this blog post, and perhaps there really isn't one. The frontend world is rapidly changing and there is no be all-end all solution. You need to look no further than this very engineering blog for posts from a couple years ago detailing how Angular JS solved the problems we had then. There's still work to be done in order to finish separating our frontend from Rails, and there will always be new and popular Javascript frameworks and libraries. For now though, the outlook of our frontend roadmap is one that will make our developers, and most importantly our users, quite happy.