Evil Front Part 1: Modern Frontend in Rails
Topics
Translations
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.
Other parts:
- Evil Front Part 1: Modern Frontend in Rails
- Evil Front Part 2: Modern Frontend in Rails
- Evil Front Part 3: Modern Frontend in Rails
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:
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.
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 asyarn.lock
which means you can add packages with a (fancier)yarn add
instead ofnpm install
. .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.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)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
, andpostcss-preset-env
.postcss-import
allows to import stylesheets from NPM packages,postcss-flexbugs-fixes
adds some workarounds for working with Flexbox, andpostcss-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 ourrails new
had created a folder namedapp/javascript
. Move it to the root of your project and rename it tofrontend
(or choose your own fancy name, but “frontend” makes most sense). Keep the insides intact:application.js
inside offrontend/packs
will serve as our Webpack “entry” point.
- Go to
application.html.erb
and replacejavascript_include_tag "application"
withjavascript_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 ourfrontend/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 theyield
statement.
-
Replace
stylesheet_link_tag 'application', media: 'all'
withstylesheet_pack_tag 'application', media: 'all'
. We are going to use CSS on a per-component basis with the help of Webpack and ES6import
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 Railsconfig
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 inapplication_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 withrails 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:
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?
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.
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";
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:
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.