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:
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.
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
. -
.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 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'
. 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
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 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:
$ 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:3000
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?
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.
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";
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 our tutorial! Stay tuned for Part 2 where our application will finally take shape and we will introduce components needed for our chat-related functionality. We will also add a helper to render our components with less typing.