Hacking Rails controller actions and rendering
Override the method_for_action method in Rails controllers to build bulk actions, render views in new ways, and make your code more secure and maintainable
![](https://terminalwire-assets.fly.storage.tigris.dev/Shared-Image-2025-02-04-16-48-28-bQTHLe0JSNUs4mW0t8Uaojl39ch5Zj96EAgIzlrLz0q98D5DYfV3fp0eVdHyEf4Tr4GFTTSSFzA0jDd2axv0dOLrLpDt93eKVHRK.png)
A common pattern Rails developers rarely have to think about is how their routes map to controller actions because it works so well and requires so little code. Rails is smart enough to know that the get "/posts/:id", to: "posts#show"
routes to the ./views/posts/show.html.erb
template. We also get nice macros like resources :posts
so there’s even less code we have to write.
Have you ever wondered how Rails handles this magic? Did you know that it’s very simple method that we can override to do some pretty nifty things in our Rails applications? In this article, we’ll explore how the method_for_action
can be used in Rails controllers to make them more readable and maintainable.
How implicit rendering works in Rails
Before we start hacking, let’s take a look at how Rails knows how to render the ./views/posts/show.html.erb
template when somebody visits /posts/1
and the action isn’t defined in the controller–it all centers around an inconspicuous method named method_for_action
. There’s not much to it.
def method_for_action(action_name) super || if template_exists?(action_name.to_s, _prefixes) "default_render" end end
When a request is dispatched to a Rails controller, it asks the controller, “Hey, before I dispatch this action, what method should I call to determine what method I should call on the controller?”
The super
method calls up to the superclass’s method_for_action
method, which checks if a method for the action was explicitly defined on the controller. If not, it then calls template_exists?
to see if a view template exists for the action.
If a template is found, the "default_render"
string is returned, which Rails uses to call on the controller. The default_render
method checks the ./app/views
directory to see if templates exist for the requested action, variant, and format.
def default_render # Look for a view template that exactly matches the requested format. if template_exists?(action_name.to_s, _prefixes, variants: request.variant) render else # ... Error handling code end end
If the template exists, Rails will render the template. If not, it will raise an error. This is the default behavior of Rails, but you can override it to do something else. There’s more code than shown above, which you can read in the implicit_render.rb Rails source code.
It’s a level of indirection, but it’s what makes implicit rendering possible. Let’s see how Superview, a component-based rendering library for Rails, hacks into the method_for_action
method to render Ruby component classes instead of templates.
Superview routes actions to Phlex or ViewComponent classes
There are a few key pieces that make implicit rendering work in Superview. Understanding how these work will both help you better wrap your head around Superview and open your brain up to other rendering possibilities in Rails.
The magic happens in method_for_action
Let’s start with the method_for_action
controller method since this is something very few Rails developers encounter.
# From the Superview gem, overrides the method_for_action method def method_for_action(action_name) super || if component_action_exists? action_name "default_component_render" end end
Rails calls the method_for_action
method to determine what method it should call when it dispatches an action to controller code. For example, if we are looking at the posts#show
action, Rails will call this method and pass the string "show"
as the argument.
Remember from above that super
checks if a PostsController#show
method is explicitly defined on the controller. If not, it then looks in the ./app/views/posts
for a corresponding show.*.*
template. This is done first to preserve the existing Rails behavior of looking for view templates in the ./app/views
directory, which means you can use Superview in existing Rails applications without changing any of your existing views.
Since we’re not using any of that, Rails finally calls the component_action_exists?
method, which checks to see if the PostsController::Show
constant exists.
# From the Superview gem, checks if a class for the action is present def component_action_class(action:) action_class = action.to_s.camelcase const_get action_class if const_defined? action_class end
If the constant is found in the controller for the action, Rails is instructed to call the default_component_render
method to render the view, which is where the component is finally rendered.
def default_component_render render component end
In this case, the component
method returns an instance of a class that responds to #render_in
, which could be a Phlex or ViewComponent instance.
That all seems a bit esoteric, and it kind of is, but understanding the method_for_action
method opens the doors to lots of other powerful abstractions, like building controllers that handle bulk updates depending on which submit
input button was clicked.
Bulk actions via submit
input buttons
To illustrate the variety of things you can do with the method_for_action
method, let’s look at how you can use it to handle bulk actions in a controller. This is a common pattern in Rails applications where you have a form with multiple submit buttons that perform different actions.
Let’s start with the view, which might look something like this:
<%= form_with url: posts_batches_path, method: :patch do |form| %> <ul> <% @posts.each do |post| %> <li> <%= form.check_box name: "batch[id][]", value: post.id %> <%= post.title %> </li> <% end %> </ul> <!-- These buttons correspond with the actions in the controller --> <%= form.submit "Publish", name: "batch[action]", value: "publish" %> <%= form.submit "Unpublish", name: "batch[action]", value: "unpublish" %> <%= form.submit "Delete", name: "batch[action]", value: "delete" %> <% end %>
The form above sends a PATCH request to the posts_batches_path
with an array of post ids and the action to perform. We’ll know which action to perform based on the value
of the submit
button the user clicks.
Here’s what a typical controller looks like in Rails that handles bulk actions.
# ./app/controllers/posts/batches_controller.rb class Posts::BatchesController < ApplicationController def update @batch = current_user.posts.find(*params.dig(:patch, :id)) case params.dig(:batch, :action) when "publish" @batch.update_all(published_at: Time.now) when "unpublish" @batch.update_all(published_at: nil) when "delete" @batch.destroy_all end end end
It is kind of weird how we have to dispatcher inside of the dispatcher. Wouldn’t it be better if we could define a method for each bulk operation on the controller? We can do that with the method_for_action
method.
# ./app/controllers/posts/batches_controller.rb class Posts::BatchesController < ApplicationController before do @batch = current_user.posts.find(*params.dig(:patch, :id)) end def publish @batch.update_all(published_at: Time.now) end def unpublish @batch.update_all(published_at: nil) end def delete @batch.destroy_all end protected def method_for_action(action_name) super || if batch_method_defined? batch_action end end def batch_action params.dig(:batch, :action) end def batch_method_defined? public_method_defined? batch_action end end
This can further be cleaned up by moving the method_for_action
method to a concern named BatchActions
.
# ./app/controllers/concerns/batch_actions.rb module BatchActions included do protected def method_for_action(action_name) super || if batch_method_defined? batch_action end end def batch_action params.dig(:batch, :action) end def batch_method_defined? public_method_defined? batch_action end end end
Then include the concern in any controller that’s goind to handle bulk actions.
# ./app/controllers/posts/batches_controller.rb class Posts::BatchesController < ApplicationController include BatchActions before do @batch = current_user.posts.find(*params.dig(:patch, :id)) end def publish @batch.update_all(published_at: Time.now) end def unpublish @batch.update_all(published_at: nil) end def delete @batch.destroy_all end end
Ahh, that looks better and cleaner! It goes further than just “clean code” though; you can add before_action
hooks for authorization, logging, or other concerns that are shared between the bulk actions, the rescue_from
method can be used to handle errors that are raised in the bulk actions, and testing should be a bit more straightforward.
Conclusion
The method_for_action
method is a powerful tool in Rails that allows you to customize how Rails dispatches actions to controller code. By overriding this method, you can clean up your controller code, make it more maintainable, and add powerful abstractions to your application.
Not only can new ways of rendering be developed, but exposing more of your application logic as actions means you can tie into it more clearly via before_action
hooks, rescue_from
error handling, and testing. Pundit policies are much easier to tie into bulk actions when the dispatcher is moved out of a case
statment and into the controller itself.
What will you build with the method_for_action
method in your Rails applications?
Support this blog 🤗
If you like what you read and want to see more articles like this, please consider using Terminalwire for your web application’s command-line interface. In under 10 minutes you can build a command-line in your favorite language and web framework, deploy it to your server, then stream it to the Terminalwire thin-client that runs on your users desktops. Terminalwire manages the binaries, installation, and updates, so you can focus on building a great CLI experience.