If you’re interested in translating or adapting this post, please email us first .

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

New! This article was updated in July 2019 to follow the latest developments in frontend and support most recent versions of Rails, Webpacker, and other libraries.

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 frontend 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 frontend 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 frontend frameworks like React and Vue that change the very way we think about frontend 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 “frontend 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 frontend 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 frontend 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 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 frontend.

Block mentality

React teaches us to think in components. Other modern frontend 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 frontend 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, postcss-preset-env, 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 frontend, 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.

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)

folder that contains all our JS dependencies (it’s also added to your 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 .

to declare all your dependencies, as well as which means you can add packages with a (fancier) instead of . .browserslistrc uses a special language to describe the list of browsers you want to target when bundling your frontend assets (e.g., “> 1%” means that you want to cater to all browsers having more than 1% market share according to Can I Use). This configuration is respected both by Babel through @babel/preset-env, and by PostCSS through postcss-preset-env. Browserslist was created by Andrey Sitnik, lead frontend developer at Evil Martians, and now adopted by Rails out of the box. You can read more about how it works here.

uses a special language to describe the list of browsers you want to target when bundling your frontend assets (e.g., “> 1%” means that you want to cater to all browsers having more than 1% market share according to Can I Use). This configuration is respected both by Babel through @babel/preset-env, and by PostCSS through postcss-preset-env. Browserslist was created by Andrey Sitnik, lead frontend developer at Evil Martians, and now adopted by Rails out of the box. You can read more about how it works here. babel.config.js contains a configuration for Babel JavaScript compiler that transforms JS files between different versions of ECMAScript language specification, enabling recent or experimental features to work in older browsers (according to Browserslist’s rules)

contains a configuration for Babel JavaScript compiler that transforms JS files between different versions of ECMAScript language specification, enabling recent or experimental features to work in older browsers (according to Browserslist’s rules) postcss.config.js сonfigures plugins for PostCSS (also created by our own Andrey Sitnik) and enables few of them out of the box: postcss-import , postcss-flexbugs-fixes , and postcss-preset-env . postcss-import allows to import stylesheets from NPM packages, postcss-flexbugs-fixes adds some workarounds for working with Flexbox, and postcss-preset-env enriches standard syntax CSS with a list of additions described here.

Webpacker auto-generates .browserslistrc with the default setting, you can see which browsers are covered by default here.

Before we move on to the rest of the setup, we need to tinker with Webpack a bit. An annoying thing about Webpacker (with an “-er”) is that it serves like an adapter between your Rails application and the “true” Webpack setup, but the configuration needs to be done according to Webpack-er settings, so every custom Webpack config needs to be wrapped in another layer of abstraction, see the official guide here.

What we want to do is to teach Webpack (through Webpacker) to treat .pcss files—there exists a convention to use this extension for files with PostCSS syntax. Using a separate extension will also allow your editor to handle syntax highlighting better. Here’s what we need to add to config/webpack/environment.js :

// config/webpack/environment.js // Add new code below the following line: const { environment } = require ( " @rails/webpacker " ); [ " css " , " moduleCss " ]. forEach ( loaderName => { const loader = environment . loaders . get ( loaderName ); loader . test = / \.( p ? css ) $/i ; environment . loaders . insert ( loaderName , loader ); }); // ...and above the following line: module . exports = environment ;

Another thing we better do right from the start is to reconfigure the default behavior 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 replace config.generators.system_tests = nil add with these lines:

# config/application.rb config . generators do | g | # Don't generate assets for Sprockets g . assets = nil # Don't generate tests and helpers (for this tutorial) g . test_framework = nil g . helper = nil 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', media: 'all' . 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 # 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:

$ bin/rails g controller pages home

# config/routes.rb Rails . application . routes . draw do root "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.pcss " ; document . body . insertAdjacentHTML ( " afterbegin " , " Webpacker works! " );

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

/* frontend/packs/application.pcss */ 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:

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 Prettier. You can configure the default style config to your own liking, but you can also stick with the one that comes out of the box.

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 development dependencies to our project. You can do this with the following command:

$ yarn add eslint \ babel-eslint \ eslint-config-airbnb-base \ eslint-config-prettier \ eslint-import-resolver-webpack \ eslint-plugin-import \ eslint-plugin-prettier \ prettier -D

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

$ touch .eslintrc.js

Put this inside:

module . exports = { extends : [ " eslint-config-airbnb-base " , " plugin:prettier/recommended " ], env : { browser : true }, 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 will be relying on stylelint to detect errors and convention violations in our stylesheets. Let’s add three more development dependencies in our package.json :

$ yarn add stylelint \ stylelint-config-standard \ stylelint-config-prettier \ stylelint-prettier -D

We will also need a stylelint.config.js file in our root — to instruct our linter.

$ touch stylelint.config.js

Inside:

module . exports = { extends : [ " stylelint-config-standard " , " stylelint-prettier/recommended " ] };

Hook me up, Lefthook

Now it is time to introduce some git hooks so your code will be automatically checked on each git commit and no faulty or untidy line will ever make to your repository, and ultimately to a production server.

For this, we will use a brand new tool developed by our engineer Alexander Abroskin: Lefthook, the fast (written in Go) and extremely flexible Git hooks manager that works naturally with JavaScript and Ruby-based projects (but can be used anywhere) and adapts to all common team workflows. It also has the prettiest console output we’ve seen in such tools.

First, we need to install it:

$ yarn @arkweid/lefthook -D

Then run a simple command that generates a lefthook.yml file in the root of our project. This is where you can configure Lefthook to your liking.

$ yarn lefthook install

Now open the resulting lefthook.yml and add this code:

pre-commit : parallel : true commands : js : glob : " *.js" run : " yarn prettier --write {staged_files} && yarn eslint {staged_files} && git add {staged_files}" css : glob : " *.{css,pcss}" run : " yarn prettier --write {staged_files} && yarn stylelint --fix {staged_files} && git add {staged_files}"

Now, every time we commit, all staged JavaScript and CSS files (including PostCSS) will be examined for errors and reformatted automatically with the help of Prettier — the de-facto industry standard for frontend linting.

I know you cannot 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.

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.pcss , 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,pcss }

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.pcss " ;

And for init/index.pcss :

/* frontend/init/index.pcss */ body { font-family : "Helvetica Neue" , Helvetica , Arial , sans-serif ; font-size : 16px ; line-height : 24px ; }

Perhaps you’ve noticed that by default different browsers use different sets of default styles, that is why the same unstyled HTML may look different in Firefox, Safari, and Chrome. To remove this headache completely, we will use the normalize.css library that will reset browser defaults and add its own, more consistent across different browsers.

We add it to our application through yarn :

$ yarn add normalize.css

Now let’s import the library at the very beginning of init/index.css so our new default styles take precedence:

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

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.pcss,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.pcss " ;

Our page.pcss has some component-related styling:

/* frontend/components/page/page.pcss */ .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 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 browser and console output for out first working component

Congratulations, you have completed Part 1 of this tutorial! If you want to compare your code with ours—feel free to refer to this branch of the official project repository on GitHub.

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 | Part 3