---
title: "Hacking Rails Implicit Rendering for View Components & Fun"
description: "Build interesting Rails plugin renderers that tap into ActionController::ImplicitRender#method_for_action like implicit renders, bulk resources, and more."
---

# Hacking Rails Implicit Rendering for View Components & Fun

![Ruby gem trapped inside of a jigsaw puzzle looking at a bunch of numbers](https://fly.io/ruby-dispatch/hacking-rails-implicit-rendering-for-view-components/assets/cover.webp)

[Annie Ruygt](https://annieruygtillustration.com)

**Understanding how Rails handles requests from routes.rb to calling the action method on a controller makes it possible to build Rails plugins ranging from Hanami-like action classes to component-driven Rails development.**

Have you ever opened a Rails controller that looks like this and wondered how it knows to render the view `./app/views/show.html.erb`?

```
class PostsController
  before_action :load_post

  # How does Rails know to render ./app/views/show.html.erb
  # when I don't write the `#show` method in my controller?

  private

  def load_post
    @post = Post.find(params[:id])
  end
end
```

Rails has a method called `method_for_action` that Rails calls to perform this magic. At first glance it doesn’t seem like much, but it turns out we can do some pretty nifty things with this method, like [build a Rails application entirely from components using nothing but Phlex classes](https://fly.io/ruby-dispatch/component-driven-development-on-rails-with-phlex/).

## Replace `def show` with `class Show`

Paste the code snippet in the controller concerns directory at `./app/controller/concerns/phlexable.rb` and you’ll be able embed Phlex views in your application controllers with the name `Show`, `Edit`, etc. to handle requests from `show`, `edit`, etc. respectively.

Let’s have a look at the entirety of the code.

```
module Phlexable
  extend ActiveSupport::Concern

  class_methods do
    # Finds a class on the controller with the same name as the action. For example,
    # `def index` would find the `Index` constant on the controller class to render
    # for the action `index`.
    def phlex_action_class(action:)
      action_class = action.to_s.camelcase
      const_get action_class if const_defined? action_class
    end
  end

  protected

  # Assigns the instance variables that are set in the controller to setter method
  # on Phlex. For example, if a controller defines @users and a Phlex class has
  # `attr_writer :users`, `attr_accessor :user`, or `def users=`, it will be automatically
  # set by this method.
  def assign_phlex_accessors(phlex_view)
    phlex_view.tap do |view|
      view_assigns.each do |variable, value|
        attr_writer_name = "#{variable}="
        view.send attr_writer_name, value if view.respond_to? attr_writer_name
      end
    end
  end

  # Initializers a Phlex view based on the action name, then assigns `view_assigns`
  # to the view.
  def phlex_action(action)
    assign_phlex_accessors self.class.phlex_action_class(action: action).new
  end

  # Phlex action for the current action.
  def phlex
    phlex_action(action_name)
  end

  # Try rendering with the regular Rails rendering methods; if those don't work
  # then try finding the Phlex class that corresponds with the action_name. If that's
  # found then tell Rails to call `default_phlex_render`.
  def method_for_action(action_name)
    super || if self.class.phlex_action_class action: action_name
               "default_phlex_render"
             end
  end

  # Renders a Phlex view for the given action, if it's present.
  def default_phlex_render
    render phlex
  end
end
```

The more interesting bit in this concern is `method_for_action`, which is what drives Rails “implicit rendering”.

Let’s break it down.

## Fly.io ❤️ Rails

Fly.io is a great way to run your Rails app close to your users. It’s really easy to get started. You can be running in minutes.

[Deploy a Rails app today! →](https://docs.fly.io/rails)

## `ActionController::ImplicitRender#method_for_action`

This is a method in Rails that Rails asks, “what method should I call to render the action given to me by the router?”. At first you might be thinking, “that’s easy! If I route to `posts#index` just call `index`! Yes, but have you ever wondered how Rails finds `./app/views/posts/index.erb` if you don’t define an index method? It does so through `method_for_action`.

For the Phlex component renderer, I call `super` first, which is Rails default rendering stack. If `super` returns nil, meaning no methods or view templates were found to render the requested action, I check if the Phlex class exists in my controller. If the class does exist, I return the string `"default_phlex_render"`, which Rails then calls to render the action.

```
# Try rendering with the regular Rails rendering methods; if those don't work
# then try finding the Phlex class that corresponds with the action_name. If that's
# found then tell Rails to call `default_phlex_render`.
def method_for_action(action_name)
  super || if self.class.phlex_action_class action: action_name
    "default_phlex_render"
  end
end
```

### `default_phlex_render`

`method_for_action` returned the `"default_phlex_render"` string, which Rails then calls via `self.send("default_phlex_render")` to create an instance of the Phlex view and pass it to Rails built-in `render` method.

```
# Renders a Phlex view for the given action, if it's present.
def default_phlex_render
  render phlex
end
```

The `phlex` method returns an instance of a Phlex class for the requested action.

```
# Initializers a Phlex view based on the action name, then assigns `view_assigns`
# to the view.
def phlex_action(action)
  assign_phlex_accessors self.class.phlex_action_class(action: action).new
end

# Phlex action for the current action.
def phlex
  phlex_action(action_name)
end
```

`action_name` is an Action Controller method that returns the action as resolved by the router. For the sake of example, let’s say we’re requesting `/blog/1/posts`—the Rails router might resolve that to the `PostsController` controller and the `index` action name. The `"index"` string gets passed into `phlox_action` method, which it turns into a class name `Index` to check if it exists at `PostsController::Index` via the `const_get` method.

```
def phlex_action_class(action:)
  action_class = action.to_s.camelcase
  const_get action_class if const_defined? action_class
end
```

If the class does exist on the controller, an instance of it is created and then we assign the instance variables from the controller into the Phlex view class *only* if it has a setter defined.

This method could be modified to also look for views in other class hierarchies, like `View::Posts::Index`, for example.

```
def assign_phlex_accessors(phlex_view)
  phlex_view.tap do |view|
    view_assigns.each do |variable, value|
      attr_writer_name = "#{variable}="
      view.send attr_writer_name, value if view.respond_to? attr_writer_name
    end
  end
end
```

Finally we copy the instance variables that were set in the controller into the class, but *only* if the Phlex view component has a setter method that matches the instance variable name. For example, `@posts` in the controller would set `Index#posts=`. Why not simply copy the instance variables from the controller into the Phlex view? Because it make the component leaky and harder to test since it breaks encapsulation. Think about it, would you rather setup a view component test like this?

```
# Eww, nobody wants to initialize a view this way
index = Index.new
index.instance_variable_set("@blogs", Blog.all)
```

Or like this?

```
# That's a little better
index = Index.new
index.blogs = Blog.all
```

The latter is easier to test and is preferred, but if for whatever reason you wanted to copy over all the instance variables directly into the class from the controller, now you know how to do it.

## Taking it further

In this example we used Phlex classes to render Rails views, but now that you know how to dispatch a request to a class embedded in a Rails controller, there’s lots that could be done including:

### ViewComponent integration

If the [ViewComponent](https://viewcomponent.org) library is your cup of tea, the same technique used to render Phlex views could be applied to the ViewComponent gem.

### Action classes

If you have code that’s heavy on `before_action` and `after_action` calls and inheriting ActionControllers doesn’t make sense, it’s possible to build [Hanami-style action classes](https://guides.hanamirb.org/v1.3/actions/overview/) that can be nested in an Action Controller. It’s not 100% the same thing, but maybe its an abstraction you need for your application.

### More sophisticated Phlex view class lookups

If you decide to build a Rails app entirely from Phlex components, you could look up constants in different name spaces like `Views::#{controller.name}::#{controller.action_name}`, or break it out into formats like `Views::#{controller.name}::#{request.format}::#{controller.action_name}`.

### Format classes

If you’re constantly implementing `format.html`, `format.json` blocks in your application, you could replace them with a responder class.

```
class ApplicationResponder
  attr_accessor :model, :controller

  delegate :action_name, :render, to: :controller

  def html
    render action_name
  end

  def json
    @model.as_json
  end
end
```

### Batch resource manipulation

Handling batches of resources in Rails in a secure manner can get awkward because Rails wants each action to have its own URL, but rendering an HTML form with a list of check boxes next to each item in a form results in a request payload that looks like this:

```
{
  batch: {
    selected: [1,2,3,4,5,6,7],
    action: "delete
  }
}
```

`method_for_action` can match `params[:batch][:action]` to the correct action in our batch controllers and make sure the endpoints are publicly accessible routes with this little snippet of code:

```
module Batchable
  extend ActiveSupport::Concern

  protected

  def method_for_action(action_name)
    routable_batch_action? ? batch_action : super
  end

  def batch_action
    params.dig("batch", "action")
  end

  def routable_batch_action?
    self.class.action_methods.include? batch_action
  end
end
```

Which we connect to our routes via this nifty little helper.

```
Rails.application.routes.draw do
  resources :blogs do
    nest :posts do
      batch :delete, :publish
    end
  end
end
```

There’s a more that goes into batch, which I’ll cover in a future post.
