When I started using Rails years ago I found helpers extremely cool. I could call a method in a view and it would help me by doing something. The method was simply there, no need to worry about its source and how to access it, just call it.
I got older, wiser, and more opinionated. I still like the concept of helpers – of methods. However, the way helpers are implemented in Rails sucks. Also, having object-disoriented functions in your view brings us back to the years where OOP still had to be invented.
In this post I’d like to discuss why I dislike Rails helpers and how to get out of that misery.
What’s a helper?
In Rails, a helper is a function.
Hello, <%= capitalize @user.name %>
#capitalize method helps me capitalizing the username, which is freaking awesome. As this is pretty simple behaviour, let’s call helpers like this utility methods. They modify the input parameter, compute something or escape strings. Pretty straight-forward.
As a second example, I’d like to show a more complex helper.
<%= render_news_for @user %>
This helper will iterate through news items for a particular user and render markup, maybe using several partials. Since it actively renders templates, let’s call this a view component.
Why are helpers shit?
At first sight, using helpers rocks. Capitalizing a string works like a charm – I simply call a function and it happens.
However, looking at the first helper I can identify several drawbacks.
- Helpers in Rails are modules, which do not allow inheritance. If I’d need a foreign method I’d have to include another module into the helper module. Not a big deal.
- Using the
#capitalize methods happens without a receiver. The method is globally available in the view since Rails somehow mixes the helper into the view. So, what happens if I have two
#capitalize methods in two different helpers mixed in the same view? I don’t have a clue. Do you?
Before getting to solutions, let me discuss another issue with helpers: Another real problem is the implementation in Rails – how these functions are made available to the view.
Helpers in Rails
Again, I’m not talking about how
#url_for are written internally, I’m talking about how these methods get into the view.
In Rails 3, all helpers are mixed into the view automatically, you still can insert additional modules using the controller’s helper facilities.
class HomeController < ApplicationController
It’s not that the implementation as-it is bad code or something, it is the idea of magically mixing methods into the view instance to make them globally available. This adds complexity to the Rails core, namely around 280 LOCs. Just to mix some methods into the view.
Helpers are shit.
I desperately tried to demonstrate the major disadvantages of helpers in Rails. To summarize.
I like utility methods. There is nothing wrong with having those little “helpers” in your view. What I don’t like is that they are called without an obvious receiver – they look and feel like functions. This is wrong.
The way Rails mixes helpers into the view is error-prone and sucks. Following a slightly different approach there’s no need for all that complexity.
Complex helpers suck. I do believe in view components and the need for those but they shouldn’t be rendering helper methods.
Moaning is fine, but let’s see how things could be changed.
Solution 1: Push Utility Methods into Decorators.
Luckily, a bunch of people feel uncomfy about the current helper architecture. My friend Steve Klabnik wrote a nice article about Jeff Casimir’s draper gem which introduces the Decorator pattern into Rails’ view layer.
Basically, the draper gem wraps existing model instances and provides utility (“helper”) methods on the decorated instance. Here’s an example.
class ArticleDecorator < ApplicationDecorator
model.published_at.strftime("%A, %B %e")
Now that we defined the Decorator we can use it to wrap the actual model.
@article = ArticleDecorator.decorate(Article.find(1))
The wrapped model can then be used in the view.
<%= @article.published_at %>
The interesting point is that we call the utility helper on the wrapped model which clearly states a receiver. No need for a homeless, global helper function. This way, we can have cleanly separated, domain-focused helpers for models. Decorators also allow inheritance and all other OOP features, since they are just objects.
Decorators are a solid technique when it comes to – well – decorating models. What can we do if there’s no matching model, for instance, when we need to call
Solution 2: Use the Controller Instance as View Context
To learn more about that we should peek at the rendering cycle in Rails. What happens when a controller renders a template?
ActionView instance is created (this will be the “context”).
- The controller manages a magical module that contains all helper methods. This module is now mixed into the
ActionView instance to make helpers available. I already discussed the need for hundreds of lines of code in order to achieve this “knowledge transfer” from the controller to the view.
- Next, instance variables from the controller are copied to the view instance as well.
These are 3 completely useless steps. Completely. Every template engine, whether it be Rails’ internal or tilt requires a so called view context whenever a template is rendered. Both instance variables and methods (that is, helper calls) used within the template are looked up on this view context instance.
Now, there is absolutely no reason for having a separate
ActionView instance as view context! We can simply use the controller instance as context object and everything would work. No need to copy over variables, no need to transfer “helpers” to the view instance.
“Helpers” would be modules mixed into the controller – and that’s it.
class HomeController < ApplicationController
@link = link_to(home_url)
Notice how we can use the mixed-in “helper” methods in the controller instance – we simply included them.
<a href="<%= home_path %>">
The cool thing is we can also use the utility methods in the view which will be invoked on the controller instance, again. No magic copying, just modules.
The Cells project currently is experimenting with this approach and things work out fine. Will blog.
I can hear people now moan about too many mixed-in methods in their ActionController – and they are right! Again, this is due to Rails’ monolithic view/controller design. If one single controller is responsible for rendering an entire web page, then this controller has a lot of responsibilities – too many. That’s why we should use Cells to split up the view into components, which is discussed next.
Solution 3: Use View Components instead of Complex Helpers.
Helpers that compute data and render partials are scary. Often, there is too much concerns in the little helper.
items = user.find_news
render "shared/news", :items => items
Let’s assume the
_news partial should be reusable throughout your application, needs some special helper function
#sanitize and does caching.
<% cache do
<%- for item in items %>
<%= sanitize item.text %>
<% end %>
<% end %>
Several problems here.
- Every controller has to take care of requiring the special
SanitizerHelper for the partial.
- Caching happens by using helpers, again, which is no good .
Moving the partial and its behaviour into a cell would cleanly separate concerns. The cell could be used as view context and thus provide utility methods itself.
class NewsCell < Cell::Base
@items = items
This creates a reusable view component with a defined scope. Intentionally, I keep the cells discussion briefly as this would break the mold.
Combining Decorators and Cells
Using draper’s decorators within cells is what I figure a fantastic option. Where the decorator cleanly wraps the model object and provides utility methods for tweaking model data the cell separates the concern into a reusable view component, provides a limited scope and generic helper methods (like
#url_for), and even caching!
I really don’t care whether draper, cells, or whatever replaces helpers – all I want is less magic code, more object-orientation and rock-solid software. This was a long post – gimme some feedback in the comments section or tweet me