Martian Chronicles
Evil Martians’ team blog
Front-end

Evil Front Part 1: Modern Front-end in Rails

An opinionated guide to modern, modular, component-based approach to handling your presentation logic in Rails that does not depend on any front-end framework. Follow our three-part tutorial to learn the bare minimum of up-to-date front-end techniques by example and finally make sense of it all.

Here’s to confused ones

Being a fresh Rails full-stack developer out in the wild is a confusing endeavor nowadays. A “classic Rails” way to handle front-end with Asset Pipeline, Sprockets, CoffeeScript and Sass looks outdated in 2017. A lot of choices made back in the times of Rails 3.1 do not live up to modern expectations. Sticking with the “old way” means passing on everything that happened in the front-end community over the past half decade: the rise of npm as a JavaScript package manager to rule them all, the emergence of ES6 as a go-to JS syntax, the winning streak of transpilers and build tools, the ever-growing embrace of PostCSS as an alternative to CSS pre-processors. Not to mention the astounding success of front-end frameworks like React and Vue that change the very way we think about front-end code: components instead of “pages”.

Trying to cram all that complexity in one developer’s head (especially for someone who is just starting out) results in a well-described cognitive fatigue.

However, the feeling of being left behind the pack, the growing difficulty to talk shop with “front-end guys”, and the creeping anxiety about job prospects should not be your only reason to question an established workflow. Programmers are rational people, after all.

What’s wrong with the Asset Pipeline?

Let’s not argue—the “old way” still works. You can still rely on a standard Rails front-end setup (and use CoffeeScript) to achieve results: your view templates, scripts, and styles will still be handled by Asset Pipeline: concatenated, minified, delivered. In production, where it all counts, they will still come in the form of two big unreadable (for humans, at least) files: one for scripts and one for styles.

As developers, however, we usually care about

  • isolated, reusable, testable code that is easy to reason about;
  • short “code change → visible result” cycle;
  • straightforward dependency management; and
  • well-maintained tools.

Sure, “classic” Rails gives our code some structure: there are separate folders for view templates, javascripts, stylesheets and images. But as the front-end complexity grows, navigating them quickly becomes a cognitive drain.

If we are not careful enough with “classic Rails full-stack way”, we end up with the global dumpster of all things CSS and JS, littered with dead code, in no time.

Speed is another reason to consider a switch. The problem is well documented and Heroku even has a dedicated guide about optimizing Asset Pipeline performance, admitting that handling assets is the slowest part of deploying a Rails app: “On average, it’s over 20x slower than installing dependencies via bundle install”.

In development, changing a line of CSS and reloading a page to see the result may also take some time—and those seconds add up quick.

What about dependencies? With Asset Pipeline, keeping them up-to-date becomes a major hassle. In order to add a JavaScript library to your project you can either load its code from CDN, cut and paste it into app/assets, lib/assets or vendor/assets, or wait for someone to wrap it into a gem. Meanwhile, JavaScript community manages the same with a single command: npm install or, most recently, yarn add . Same goes for updating. Yarn gives us the convenience of Bundler—for JavaScript.

Finally, Sprockets, the build tool behind Asset Pipeline, does not look well-maintained, and for quite some time:

Sprockets and Webpack GitHub pulse

Sprockets have become a bit rusty over the last 5 years (left). Webpack’s pulse over the same time frame (right)

Wind of change

In 2017, DHH and Rails community have finally started changing things around. Rails 5.1 brought us Webpack integration with the webpacker gem, node_modules through Yarn, out-of-the-box support for Babel, React, Vue and PostCSS (and even for Elm, if you are feeling adventurous).

Asset Pipeline and CoffeeScript, however, still maintain their hold: starting a project with bare rails new gives you the “good old way”. While searching the web for JS-related topics, you still have to transpile code examples in your head in order to make any sense of them.

Don’t fret, though, as your Rails app can adopt all the modern practices now, and we are going to cover the basics together. All you need to start is some basic knowledge of Rails, JavaScript and CSS. We will also leverage latest Rails 5.1+ features to keep configuration and tooling to the minimum.

In this series of tutorials, we will share some of the best practices developed at Evil Martians to build a modern sensible front-end.

Block mentality

React teaches us to think in components. Other modern front-end frameworks follow the lead. Modularity is the philosophy behind common CSS methodologies such as BEM. The idea is simple: every logical part of your UI should be self-contained.

Rails has a built-in way to break your views into logical parts—view partials. But if your partial relies on JavaScript, as any modern component probably does, you have to reach for it in a far-away folder, under app/assets/javascripts.

What if we could bring everything together and have partials, their respective scripts and styles together—in the same place?

That way we can rely on the smarts of modern build tools to only bundle the components we actually use. And whenever we want to change something—we know exactly where to look.

The approach we are going to showcase does not rely on React, Vue or Elm architecture, and purposefully so: you are free to learn those tools on your own, but you don’t have to take a steep learning curve right now. You can use tools that already come with Rails to gradually adopt a modern front-end mindset.

Sass vs. PostCSS

Rails loves Sass. We, however, tend to stick to PostCSS. First of all, it is 36.4 times faster than the built-in Ruby Sass that handles CSS processing in Rails. It is written in 100% pure JavaScript. It is easily extendable and customisable with numerous plugins. One of them, cssnext, comes out of the box and generates polyfills for features that are not supported by browsers yet, but only as long as it is necessary. And you can still use PostCSS on top of your favorite pre-processor—if you ever find a reason for that.

What are we building?

It is finally time to get our hands dirty. To demonstrate a new approach to front-end, we will build a standard run-of-the-mill chat application with minimal authentication and ActionCable. Let’s call it evil_chat. The example is not too complex, but is still sophisticated enough to make our experience “full-stack”.

In our project, we are going to say goodbye to Assets Pipeline and default Rails generators that create a bunch of .scss and .coffee files. We are going to keep ERB as the default templating engine, leaving you to explore alternatives like Slim or Haml at your own pace.

A new frontend folder in your app

On the left, folder structure for Evil Front

We are also going to revisit the folder structure. Everything will now happen in the new frontend folder at the top level of our application. It will replace app/assets completely.

Don’t worry if it does not quite make sense yet, let’s take it step by step.

How do I start my project?

So, bare rails new doesn’t cut it anymore. Here is your new magic line (we assume the app’s name is evil_chat):

$ rails new evil_chat --skip-coffee --skip-sprockets --skip-turbolinks --webpack --database=postgresql  -T

As you see, we no longer need CoffeeScript or any of the Sprockets-related functionality. -T is optional, it skips creating test files, as testing is beyond the scope of this tutorial. We will use PostgreSQL as our default database with --database=postgresql, as it will make our app easier to deploy on Heroku once we’re done.

The most important option is --webpack. It tells Rails to use the webpacker gem to bundle all our assets with Webpack. Now our project comes with a set of modern tools:

  • A node_modules folder that contains all our JS dependencies (it’s also added to your .gitignore so you don’t commit thousand of extra files in your repo by mistake)
  • A package.json to declare all your dependencies, as well as yarn.lock which means you can add packages with a (fancier) yarn add instead of npm install.
  • .babelrc file configured for transforming ES6 into JavaScript code compliant with any browser that currently has more than 1% of market share.
  • .postcssrc.yml already configured with postcss-smart-import and postcss-cssnext plugins that allow you to use all the features described in cssnext.

Some things are forgotten, though. Notably, a global config for browserslist, that tools like Autoprefixer are going to need to correctly process your code to be cross-browser compliant. Gladly that one is easy to fix, just create a file in your project’s root:

$ touch .browserslistrc

Well, not really, but you can certainly get away with this much knowledge

Now open this file and add a single line: > 1%. That’s all there is to know about browser compatibility!

Another thing we better do right from the start is to reconfigure the default behaviour of Rails generators. We don’t need them to put anything into app/assets, as (spoiler!) we are going to remove this folder altogether in a next step. Open application.rb and add these lines:

# config/application.rb
config.generators do |g|
  g.test_framework  false
  g.stylesheets     false
  g.javascripts     false
  g.helper          false
  g.channel         assets: false
end

Time to perform the desecration of Asset Pipeline. Remove the app/assets folder.

But how do we replace it? Follow these steps:

  • --webpack option in our rails new had created a folder named app/javascript. Move it to the root of your project and rename it to frontend (or choose your own fancy name, but “frontend” makes most sense). Keep the insides intact: application.js inside of frontend/packs will serve as our Webpack “entry” point.

  • Go to application.html.erb and replace javascript_include_tag "application" with javascript_pack_tag "application". One word in a method name makes all the difference: include_tag inserts a reference to an app-wide JavaScript file compiled by Sprockets (old way), pack_tag brings in a Webpack bundle generated from the entry point, which is our frontend/packs/application.js (new way). While at it, move the pack tag down from the <head> to the very end of the <body>, right after the yield statement.

  • Replace stylesheet_link_tag 'application', media: 'all' with stylesheet_pack_tag 'application'. We are going to use CSS on a per-component basis with the help of Webpack and ES6 import statement. That means all our styles will be handled by webpacker too.

  • Now we need to let webpacker know where to look for files to bundle, as we have just renamed the default folder. As of webpacker 3.0, configuration is done through webpacker.yml file inside of Rails config folder. Make sure first few lines look like these to reflect the change in our folder structure:

default: &default
  source_path: frontend
  source_entry_path: packs
  public_output_path: packs
  cache_path: tmp/cache/webpacker
  • Our ERB partials are going to live in frontend folder as well, and our controllers wouldn’t know how to find them, unless we tell them so in application_controller.rb:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  # That's all there is:
  prepend_view_path Rails.root.join("frontend")
end
  • As of webpacker 3.0, we no longer need a separate process to compile assets on-demand in development, but if we want to make use of automatic page refresh on every change in JS/CSS code, we still need to run webpacker-dev-server alongside with rails s. For that we need a Procfile, so let’s create one:
$ touch Procfile

Put this inside:

server: bin/rails server
assets: bin/webpack-dev-server

With Procfile in place, you can launch all your processes with a single command using a tool like Foreman, but we highly recommend using our alternative: the Hivemind. You can also take a look at its big brother Overmind, as it will allow you to use pry for debugging without interrupting any running processes.

Smoke test

Time to test if our new setup is working correctly. Let’s add some simple code to our application.js (found under packs) to manipulate our DOM and then make sure webpacker handles it well. First, we need to generate a basic controller and provide a default route:

$ rails g controller pages home
# config/routes.rb
Rails.application.routes.draw do
  root to: "pages#home"
end

Make sure to remove everything from views/pages/home.html.erb, so it contains no code at all. Now in application.js remove everything that is there and replace it with this:

// frontend/packs/application.js
import "./application.css";

document.body.insertAdjacentHTML("afterbegin", "Webpacker works!");

Let’s also create an application.css file in the same folder to check that our styles are processed too (with PostCSS):

/* frontend/packs/application.css */
html, body {
  background: lightyellow;
}

Time to launch our server for the first time! We assume you already have Hivemind installed, if not—use foreman or a similar process manager (but, seriously, consider Hivemind, it’s awesome).

$ hivemind

Now go to http://localhost:5000 (Hivemind sets the $PORT environment variable to 5000 and Rails uses the same variable to determine which port to run on) and see the result:

A smoke test for our app

If it does not burn, it works!

And here is a cool little thing about Webpack. If you go to application.js, change “Webpacker works!” to something else and save the file, you will see changes in your browser without having to hit a “Refresh” button.

Now, before we start writing any real code, let’s make sure we write it in style.

Okay, how do I lint my JS?

Prettier also integrates with all popular editors so you can reformat your code with a touch of a button. ESLint also has plugins for all main editors to give you instant visual feedback.

There are so many ways to write JavaScript and with syntax being updated on yearly basis now, it is so easy to get confused before you even start. The semicolons/no semicolons debate never gets old, for instance. Instead of arguing over each peculiarity of JavaScript syntax, it’s easier to stick with some opinionated code formatter, such as Standard or Prettier. We choose Prettier (and yes, it has semicolons by default, but you can easily turn it off).

We are going to set up some automated linting with ESLint, so our code style is always kept in check. We are also going to rely on Airbnb JavaScript Style Guide that contains a lot of best practices for writing maintainable JS code.

Let’s add some devDependencies to our package.json, as for now it only contains webpack-dev-server. This is how it should look like after we make it cater to our JS linting needs:

{
  "name": "evil_chat_codealong",
  "private": true,
  "dependencies": {
    "@rails/webpacker": "^3.0.1"
  },
  "devDependencies": {
    "webpack-dev-server": "^2.9.1",
    "babel-eslint": "^8.0.1",
    "eslint": "^4.8.0",
    "eslint-config-airbnb-base": "^12.0.1",
    "eslint-config-prettier": "^2.6.0",
    "eslint-import-resolver-webpack": "^0.8.3",
    "eslint-plugin-import": "^2.7.0",
    "eslint-plugin-prettier": "^2.3.1",
    "lint-staged": "^4.2.3",
    "pre-commit": "^1.2.2",
    "prettier": "^1.7.3"
  }
}

lint-staged and pre-commit will come handy later, when we add some hooks to our git add and git commit commands. This way we make sure that less-then-ideal code will never even make it to a repository.

One last touch: we need .eslintrc file in our root folder so ESLint knows how to apply our rules.

$ touch .eslintrc

Put this inside:

{
  "extends": ["eslint-config-airbnb-base", "prettier"],

  "plugins": ["prettier"],

  "env": {
    "browser": true
  },

  "rules": {
    "prettier/prettier": "error"
  },

  "parser": "babel-eslint",

  "settings": {
    "import/resolver": {
      "webpack": {
        "config": {
          "resolve": {
            "modules": ["frontend", "node_modules"]
          }
        }
      }
    }
  }
}

The order of elements under "extends" key is important: this way we are telling ESLint to first apply Airbnb rules, and whenever there is a conflict with Prettier format guides, prefer the latest. We also need to add a key "import/resolver" for our eslint-import-resolver-webpack dependency: it makes sure that whatever you import in your JS files actually exists in the folders handled by Webpack (in our case, it’s a frontend folder).

What about CSS?

CSS needs some linting too! We are also going to normalize it with a well-respected tool normalize.css. We will be relying on stylelint to detect errors and convention violations in our stylesheets. Let’s add two more development dependencies in our package.json:

"devDependencies": {
    ...
    "stylelint": "^8.1.1",
    "stylelint-config-standard": "^17.0.0"
  }

We will also need a .stylelintrc file in our root — to instruct our linter.

$ touch .stylelintrc

Inside:

{
  "extends": "stylelint-config-standard"
}

Also, add normalize.css under "dependencies" key in package.json (not devDevdependencies this time!), so that part of your package listing looks like this:

"dependencies": {
    "@rails/webpacker": "^3.0.1",
    "normalize.css": "^7.0.0"
  },
  ...

Now it is time to introduce some git hooks so all checks will run automatically on each git commit. For that, we will add a "scripts" key to our package.json:

...
"scripts": {
    "lint-staged": "$(yarn bin)/lint-staged"
  },
  "lint-staged": {
    "config/webpack/**/*.js": [
      "prettier --write",
      "eslint",
      "git add"
    ],
    "frontend/**/*.js": [
      "prettier --write",
      "eslint",
      "git add"
    ],
    "frontend/**/*.css": [
      "prettier --write",
      "stylelint --fix",
      "git add"
    ]
  },
  "pre-commit": [
    "lint-staged"
  ],
  ...

Now, every time we commit, all staged files will be examined for errors and reformatted automatically.

Our final package.json should look like the contents of this gist.
To install all new dependencies, run yarn in your Terminal.

I know you can not wait to see our automated linting in action. Try going to your frontend/packs/application.js and removing a semicolon. Then run git add . && git commit -m "testing JS linting" and see that semicolon being added right back. See? No sloppy style anymore.

Linter's config

If everything was set up correctly, our project’s root should contain all those files

Our first component (no React involved)

Just to give you a taste of what will be happening in Part 2 of this guide, let’s create our first component.

First, let’s get rid of our application.css, we only needed that one for a smoke test. Delete all code from application.js too. From now on, our application.js will only contain import statements. This our entry point, a place where everything comes together. We will need some other place to keep app-wide stylesheets and javascripts, so let’s create one. We will call this new folder init.

$ mkdir frontend/init
$ touch frontend/init/index.js
$ touch frontend/init/index.css

Now we need to register our new folder inside our entry point. Add this line to your packs/application.js:

// frontend/packs/application.js
import "init";

And now some code for our new files. Here is our init/index.js:

// frontend/init/index.js
import "./index.css";

And for init/index.css:

/* frontend/init/index.css */
@import "normalize.css/normalize.css";

body {
  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-size: 16px;
  line-height: 24px;
}

All we do here is applying some general styling to all fonts in our app. Our init folder will also be the first to go into the bundle, so it makes sense to include our normalize.css here. Later we can use the same folder to set up polyfills or error monitoring—any functionality that does not relate directly to our components and needs to be loaded as soon as possible.

Okay, init is a special case, so what about the components?

Each component is a folder with three files in it: one for ERB partial, one for scripts, and one for styles.

All our components will be located in the components folder inside our frontend. Let’s create one, along with the first component that will simply be called page (think of it as a template for our layout):

$ mkdir -p frontend/components/page
$ touch frontend/components/page/{_page.html.erb,page.css,page.js}

Note that we are not calling our component’s JS file index.js, this name is reserved for our init folder. We choose to name our JS files the same as our components so that later, when we have multiple open tabs in our editor, we can quickly figure out where we are. This practice is not common (in other tutorials you will see mostly index.js for components), but saves a lot of time when writing code.

We don’t have any component-related JS logic yet, so our page.js still consists of a single import statement for a CSS file:

// frontend/components/page/page.js
import "./page.css";

Our page.css has some component-related styling:

/* frontend/components/page/page.css */
.page {
  height: 100vh;
  width: 700px;
  margin: 0 auto;
  overflow: hidden;
}

Finally, our _page.html.erb contains markup. Note the we can use all ERB goodies here and leverage the yield statement that will allow us to nest components one inside another.

<!-- frontend/components/page/_page.html.erb -->
<div class="page">
  <%= yield %>
</div>

Don’t forget to reference our new component in application.js by adding import "components/page/page";

A structure for the first component

A structure of our “frontend” folder at this point of tutorial

Now let’s add some ERB code to our home.html.erb view:

<!-- app/views/pages/home.html.erb -->
<%= render "components/page/page" do %>
  <p>Hello from our first component!</p>
<% end %>

Time for to see our first component in action! Launch the server again and refresh the page. Fingers crossed, you are going to see something like that:

A structure for the first component

A browser and console output for out first working component


Congratulations, you have completed Part 1 of this tutorial! Please, proceed to Part 2 where our application finally takes shape and we introduce components needed for our chat-related functionality. We also add a helper to render our components with less typing and a generator to automate their creation.


Part 1 | Part 2