Including Rails View Helpers is a concern
If you’re currently maintaining a Ruby on Rails codebase, I want you to do a quick regex code search in your Editor:
include .*Helper
Did you get any hits? Do any of those constants point back to your app/helpers
directory? That could be a problem.
Never include a module from app/helpers
into anything in your application. Don’t do it.
- Modules defined in
app/views
should exclusively be View Helpers. Every module in theapp/views
directory is automatically included into Views/Partials, and available within Controllers via thehelpers
proxy e.g.helpers.the_method
in Controllers orApplicationController.helpers.the_method
anywhere else. - Including View Helpers into other files (Controllers, Models, etc.) creates a risk that some methods may not be safely callable because they depend on View Context that isn’t present. (They’re also hell to type with Sorbet.)
- If you do have includable mixins (“bucket of methods”) that do make sense to be included into lots of different classes (Controllers, Models, Views, etc.), make them a concern and don’t put them in
app/helpers
.
Some general background
Rails has always had View Helpers. Prior to Rails 2 (~2009), only the ApplicationHelper
was included into all controller views and other helpers would have to be added manually. Rails 2 changed the defaults via helpers :all
and config.action_controller.include_all_helpers
to always include all Helpers in all Views.
Rails 4.0 (2012) introduced Concerns, which formalized conventions around extracting shared behaviors into module mix-ins.
Rails 5.0 (2016) introduced the Action Controller helpers
proxy, and clearly summarizes the problem that I’ve observed too:
It is a common pattern in the Rails community that when people want to use any kind of helper that is defined inside app/helpers they includes the helper module inside the controller like:
module UserHelper def my_user_helper # ... end end class UsersController < ApplicationController include UserHelper def index render inline: my_user_helper end end
This has problem because the helper can’t access anything that is defined in the view level context class.
Also all public methods of the helper become available in the controller what can lead to undesirable methods being routed and behaving as actions.
Also if you helper depends on other helpers or even Action View helpers you need to include each one of these dependencies in your controller otherwise your helper is not going to work.
Some specific background
This has come up as a problem at my day job, GitHub. GitHub has the unique experience of being one of the oldest and largest Ruby on Rails monoliths and it’s full of opportunities to identify friction, waste, and toil.
Disordered usage of View Helpers and the app/views
directory became very visible as we’ve been typing our monolith with Sorbet. Typing module mixins in Sorbet is itself inherently difficult, but View Helpers had accumlated a significant amount of T.unsafe
escape-hatches and in understanding why… we discovered that explicitly including View Helpers in lots of different types of classes was a cause.
What’s the alternative?
I analyzed the different types of modules that were being created, and came up with this list:
- Concerns are shared behaviors that may be optionally included into multiple other classes/objects when that behavior is desired. We can further break down:
- Application-level Concerns are agnostic about the kind of object they are included into (could be a controller, or model, or a job, or a PORO)
- Component-level Concerns are intended to only be mixed into a specific kind of object, like a controller with expectations that controller-methods are available to be used in that concern (like an http request object, or other view helpers like path helpers)
- Dependencies are non-shared behaviors that have been extracted into a module from a specific, singular controller to improve behavioral cohesion, and is then included back into that one, specific class or object.
- View Helpers are intended to be across Views (or Controllers via the
helpers
view-proxy method in Controllers orApplicationController.helpers
anywhere else) for formatting and presentation purposes and have access to other view helpers or http request objects. These are the only objects that modules that should go inapp/helpers
.
And this is what you might do about them:
- Stop and remove
include MyHelper
from Controllers. Instead, you can access any View Helper method in a controller viahelpers.the_name_of_the_method
- Move Concerns and Dependencies out of
app/helpers
. If what is currently inapp/helpers
is not a View Helper, move it:- Application-level Concerns should be moved into
app/concerns
- Component-level Concerns should be moved into their appropriate
app/controllers/concerns
oryour_package/models/concerns
, etc. - Dependencies should be moved to an appropriate place in their namespaces hierarchy. e.g. if the module is only included into
ApplicationController
, it should be namedApplicationController::TheBehavior
and live inapp/controllers/application_controller/the_behavior.rb
- Application-level Concerns should be moved into
- Never include a module from
app/helpers
anywhere. Don’t do it. - Use the Controller
helpers
proxy orApplicationController.helpers.the_helper_method
to access helpers (likeActionView::Helpers::DateHelper
) in Controller or other Object contexts. - Invert the relationship between Helpers and Concerns. If you have behavior that you want available to lots of different kinds of components and views, start by creating a Concern, and then include that Concern into a View Helper or
ApplicationHelper
. Don’t go the other direction. - Invert the relationship between Views and Controllers. If you have a private method that is specific to a single controller, and you want to expose that method to the controller’s views, you can expose that method to the views directly using
helper_method :the_method_name
. Use this sparingly, because it extends singleton View objects which deoptimizes the Ruby VM; but really, don’t twist yourself into knots to avoid it either, that’s what it’s there for. - (optional but recommended) Rename the constant too, not just move it, when it’s not a View Helper. Naming things is hard, but
*Helper
is… not very descriptive. While it’s the placement inapp/helpers
that brings the automatic behavior… so it’s not technically a problem to have aSomethingHelper
that isn’t a View Helper living inapp/controllers/concerns
… it is confusing to have non-helpers namedSomethingHelper
. Some suggestions for renaming Concerns and Dependencies:- Use the “-able” suffix to turn the behavior or capability into an adjective. e.g.
SoftDeletable
- Append
Dependancy
to the end, likeAbilityDependency
- If you’re out of ideas, use
Methods
orMixin
, likeUserMethods
orUserMixin
.
- Use the “-able” suffix to turn the behavior or capability into an adjective. e.g.