Phoenix vs Rails: Views and helpers

SeriesPhoenix on Rails clock10 min read

Here's an overview, case study and comparison to Phoenix of the V part of Rails MVC as experienced across the years.

Rails organizes the view part of the MVC into views and helpers. You are supposed to put templates for each resource/controller into the app/views subdirectories and to put the accompanying helper code into app/helpers which are by default generated per-resource as well.

In practice, I saw many serious projects in which this tends to be one of the most crowded and unorganized places of a project. And for a reason: this is a place where the code of back-end and front-end developers meets. That means you could really use some good conventions and patterns ruling it. Otherwise, implementation and maintenance of both server-side and client-side features will become ridiculously slow and bug-prone.

Now, I believe Phoenix is not only cool because Erlang VM and its concurrency model nail down the web request handling problem. It's a serious improvement because it was carefully designed to solve many little problems that haunt Rails MVC for years. And so it happens that the view layer is a perfect case to show this. 

Case by case comparison

Straight to the point. I'll show case by case what's exactly wrong with the V part of the Rails MVC according to my Rails project experiences and how does Phoenix compare in each one.

A. Everything, everywhere

Let's start with a simple resource-specific Rails helper:

lang:ruby
module UsersHelper
  def user_status(user)
    user.confirmed? ? "Active since #{user.confirmed_at}" : "Inactive"
  end
end

We start with using user_status just in app/views/users. But then, if need be, we also begin using it in other views because nothing forbids us from doing so. Because every helper is available everywhere. There's no reason, other than painful experience, to think we're doing something wrong by using helpers across resources. But then, we start putting helpers into random places that "seem" to be most fitting. We start losing a sense of organization.

In Phoenix that's not possible because every template has access only to functions in its view module, simply because that template is also a function in particular view module. If we need to reuse some view helper across multiple resources, we'll be forced to extract it to its own module and explicitly import that module wherever needed. 

lang:elixir
defmodule MyApp.UserInfo do
  def user_status(%User{ confirmed_at: nil }), do: "Inactive"
  def user_status(%User{ confirmed_at: time }), do: "Active since #{time}"
end

defmodule MyApp.UserView do
  import UserInfo, only: [ user_status: 1 ]
end

defmodule MyApp.Admin.UserView do
  import UserInfo, only: [ user_status: 1 ]
end

Opposite to Rails, this is not just an approach of experienced Phoenix developer but of every Phoenix developer. There's simply no other way here.

Edit: As pointed out by Xavier Noria in comment below the article, global helper availability is a default that can be changed via the action_controller.include_all_helpers setting. It's only fair to include this info, so thanks Xavier! As I haven't seen or worked with Rails projects using this option, I encourage anyone who did to join the conversation.

B. Everything in ApplicationHelper

We're still lucky if helpers are properly grouped by resources in Rails because often all helpers end up in ApplicationHelper, which then makes for hundreds or thousands of lines. We can learn not to do that, to go with presenters, decorators etc, but Ruby/Rails do nothing to show us why. Again, we need to learn it the hard way, because all helpers are available everywhere so there's no language-backed reason to care about creating multiple helper files.

This is not possible in Phoenix because, as stated in case A, views are per-resource with no global dumpster like the ApplicationHelper in Rails.

C. Name collisions

Here's an fast lane to having a bug in Rails view code:

lang:ruby
module UsersHelper
  def avatar_container(user)
    # ...
  end
end

module AdminsHelper
  def avatar_container(admin)
    # ...
  end
end

Seriously, it's not that hard to first assume the avatar_container method name is unique enough and so strictly related to users that it's OK for a helper name, and next to forget about that when admins get added to the picture and they also need avatars. Guess what, you won't even get a warning from Rails about that. In Phoenix, this will never compile.

Also, due to the sole perspective of possible collision, many self-conscious Rails devs including me start to prefix their helper methods with the helper name. That may be fine for UserHelper but what about AdminNotificationsHelper or any other SeriouslyLongNamedHelper? And what about namespaces? In Phoenix, you can go with an alias instead of import and use UserInfo.status instead of user_status. The choice is yours.

D. Instance variable collisions

As Rails helpers live in a magical context that travels between the controller and the view layers, you may end up coding something like below:

lang:ruby
module UsersHelper
  def notification_count(user)
    @notification_count ||= # some hardcore DB query
  end
end

class NotificationsController < ApplicationController
  def index
    @notifications = current_user.notifications.unseen
    @notification_count = @notifications.total_count
  end
end

For sure, you'll enjoy your time spent on finding bugs that will follow this accidental definition of the instance variable @notification_count twice. First, it was used for caching purposes in the helper but then for regular controller-template communication in the controller. There may also be similar collisions between helpers, both living in same module or in separate ones.

This is where Phoenix benefits from the explicit data flow and lack of per-request global state - the core functional programming features brought by Elixir. You pass data both between controller and view or between template and view in an explicit way. This way you always know what the view code works on and where it came from. Speaking of which...

E. Seriously, where did it come from?

We've already established that Rails helpers live in a global dumpster-like space where everything is mixed together so it's often hard to figure out where a particular method lives without project-wide search.

But that's not all. A heavy load of ActiveSupport methods and methods introduced by other gems join this space too. How would you know where the number_to_currency method comes from if not by web search, IDE feature or existing experience with Rails code/documentation? Neither of these things is your actual code, so there's just no relevant trace in the project code.

In Phoenix, answers to these questions usually live either in web.ex, because that's where you can extend macros that contribute to your controllers, models, views etc, or straight in your project's modules. You may have grown to appreciate Rails magical extension skills but believe me - it's really not that much of an overhead to have it all explicit and it makes the project feel much more organized and approachable for new team members.

F. Compilation, performance and stupid errors

You may have the following ERb/EEx view both in Rails and Phoenix:

lang:html
<% if @user.confirmed? %>
  <div class="confirmed"><%= display_name(@user) %></div>
<% else %>
  <div class="inactive"><%= display_mame(@user) %></div>
<% end %>

Can you spot the problem? In Rails you'll have to get to that else block either in development, on staging or in tests in order to discover the display_mame typo (or did you really go after the arcade game simulator?). But not in Phoenix. Here, the template will be converted by macro system into regular function in the relevant view module. Then, they'll get compiled as such and it'll throw a compilation error because there's no display_mame function defined.

Templates really feel like a first-class citizens in the Phoenix project's codebase because they're just a regular compiled code. But that's not all. Because templates end up as functions, which incidentally are the most basic and lightning fast entities in Elixir, they're in no way less performant than any other part of your web app. And the choice of template engine will not be a choice of performance here. At least not as much as in Rails.

G. MVC apocalypse

This may seem so obviously flawed on paper, but I'm sure you've witnessed or written the following at least once in your Rails developer lifetime:

lang:ruby
class User < ActiveRecord::Base
  include ApplicationHelper

  # ...
end

Yes, nothing forbids you to ruin the MVC in any MVC framework, but I believe that this problem happens in Rails particularly often and for a particular reason. That reason is a lack of built-in solution for truly organizing the view code mixed together with an easy, built-in solution for screwing things up. 

Even if we really, really needed to pull that one precious thing from the view layer and we didn't care to extract it into its own facility, here's the worst you can end up with in Phoenix:

lang:elixir
defmodule UserView do
  def user_can_send_notifications?(user), do: true

  # ...other methods, not needed in the model
end

defmodule User do
  import UserView, only: [ user_can_send_notifications: 1 ]
end

There's no global equivalent of ApplicationHelper so we'd probably end up reaching some more specific module like the UserView here. Also, imports are usually written to target specific methods only so we'll be easily able to see why this compromise was made. And there's always a compiler with unused method warnings at bay in case we'll still get lost.

H. JSON and serializers

OK, so every web framework sets us up when it comes to rendering HTML by giving us a nice, cozy place for our templates. But what about JSON or other data formats?

In Rails, because there's no app/templates and it's the app/views that contains the templates, the most natively fitting choice seems to use a template engine for needed format. These tend to be slow (like jbuilder) or just not to express the format as well as regular code. 

So alternatively, you can introduce app/serializers or other custom member of the project structure. But then, how does that connect to the controller code? In case of a popular active_model_serializers gem, it's kind of a magical connection to the matching model that works best when we're only rendering ActiveRecord models and we're always rendering data for particular model in the same way. Otherwise, we'll end up overriding this magic.

Again, Phoenix reaches out to Elixir to solve this in a native, compiler-friendly way. Having proper view modules, you don't have to invent another layer or reach out to DSLs. There, you can just write methods that map your data structures to JSON objects. And you can just move reusable parts into separate modules. Then, you can just invoke them wherever needed. Nothing more than daily Elixir coding routine. And there's no performance penalty whatsoever.

Summary

There are many issues with the Rails view layer that you learn about, and sometimes learn to live with, as years go by. Rails community has developed the presenter pattern to rectify some of them. And it improves things a great deal. But, just like serializers, it does so by introducing a new DSL getting further away from the original Ruby syntax and interpreter support. And of course, you'll only find this pattern in those projects lucky enough to have it applied in.

Phoenix leverages on explicitness of Elixir imports and the versatility of macro-powered compiler to guarantee a much better starting quality of the view code. Here are some highlights:

  1. Views are simply Elixir modules which are your natural first stop for putting your view code, regardless of the format. Being native Elixir modules, they let you organize your view code just like any other Elixir code. And you'll always know where things come from. 
  2. As there's no global per-request state, this also applies to data used by both helpers and templates. You'll only use what you've deliberately passed there.
  3. By compiling templates into view module functions and physically putting both next to each other, the link between the view code and the template is truly tangible.

Oh, and don't forget view related asset management features like code reloading, explained in detail in Rails asset pipeline vs Phoenix and Brunch article.

As always, please consider leaving a comment about your experiences with view layers in both frameworks and your preferences about them.