How Does Rails Assign Variables to Rendered Views?

If you enjoy this article you may be interested in the book I am working on called Building and Deploying Crypto Trading Bots.

Throughout this post, we will investigate one source of that "Rails magic" that perplexes developers: how Rails assigns Controller instance variables to view templates.

Take the following for example:

# If you inspect action_controller.rb you will find it inerhits from ActionController::Base
class MyController < ApplicationController
 def index
  @my_index_var = [1,2,3,4]
 end
end
app/controller/my_controller.rb
<% @my_index_var.each do |i| %>
  <p> Number: <%= I %> </p>
<% end %>
app/views/my_controller/index.html.erb

Have you ever wondered how on earth the view template index.html.erb gets access to the instance variable @my_index_var ? Well, let's find out.

What is Rails Anyway?

Without jumping too far into Rails source code (yet), recall that the Ruby on Rails source code isn't a massive monolithic code base. Rather, it is a collection of isolated Ruby gems that are strung together to make up the tool we call Rails. Inside the  codebase, the responsibilities of template rendering, rack request routing, object relational mapping and more are divided across  several key gems that are usually prefixed with Action* or Active* . All these gems work together to deliver the end framework you and I use for web application development. With the primer out of the way, let's begin the journey to expose how controller instance variables are assigned to views.

ActionPack'd

Our first stop is the gem called ActionPack. ActionPack defines several important modules including ActionController, ActionDispatch and AbstractController. Thematically the gem revolves around framework controller code, rendering and routing of rack requests.

For our investigation, the first class in ActionPack to take a look at is  ActionController::Base . This is the base class that all your application controllers will inherit from.

class ApplicationController < ActionController::Base
end

class Posts < ApplicationController
end

In essence ActionController::Base is the core of a web request in Rails. It gives controllers the ability to define actions for requests, and have requests routed to those actions in order to render a template or redirecting somewhere else. ActionController::Base inherits from a parent class ActionController::Metal which in turn inherits from AbstractController::Base. ActionController::Metal isn't very interesting so we won't spend too much time on it. The in-line source comments describes it as:

... the simplest possible controller, providing a valid Rack interface without the additional niceties provided by ActionController::Base

Circling back to ActionController::Base, the class itself doesn't actually define many interesting methods. Instead ActionController::Base is a composite of various modules such as UrlFor, Redirecting, HttpAuthentication, Logging, Cookies that are loaded into the class at require time. One module in deserving of our focus is  AbstractController::Rendering.

The AbstractController::Rendering module provides ActionController a variety of handy methods including the render method we know and love.

def render(*args, &block)
  options = _normalize_render(*args, &block)
  rendered_body = render_to_body(options)
  if options[:html]
    _set_html_content_type
  else
    _set_rendered_content_type rendered_format
  end
  _set_vary_header
  self.response_body = rendered_body
end
https://github.com/rails/rails/blob/661da266b94909574426fd1121ef13b800e01b9a/actionpack/lib/abstract_controller/rendering.rb#L23
class Posts < ApplicationController
  def new
    render 'new'
  end
end

In addition to render, the module also contains a method called view_assigns . The view_assigns method is pretty quirky:

# This method should return a hash with assigns.
# You can overwrite this configuration per controller.
def view_assigns
  variables = instance_variables - _protected_ivars

  variables.each_with_object({}) do |name, hash|
    hash[name.slice(1, name.length)] = instance_variable_get(name)
  end
end
https://github.com/rails/rails/blob/661da266b94909574426fd1121ef13b800e01b9a/actionpack/lib/abstract_controller/rendering.rb#L63

Basically, it calls the Ruby base Object method instance_variables which returns an Array of all the currently defined instance variables for an object and creates a hash mapping their names to their values.  Now, recall that AbstractController::Rendering is a module that is mixed in the class ActionController::Base which is what your concrete controller implementation inherits from. This means that if view_assigns is invoked from your controller all the currently defined instance variables will be assigned to this hash. Interesting.... We've discovered how the instance variables are captured but how does the controller connect to the view?

ActionView

Returning to our previous note about Rails being built from "Action" type gems, view and templating logic live in a fun little gem called ActionView. ActionView is responsible for understanding how to render different template engines like embedded ruby, and HTML. To draw the lines of responsibility a bit more clearly, ActionController can tell us what to render, but, it does not know how to render it. That's ActionView's job.

The base class for ActionView is somewhat anti-climatically named ActionView::Base. The class itself does quite a bit of serious business. The job of hierarchal template rendering doesn't sound like a laughing matter (but then again maybe it is? DHH seems to have a lot of fun on Twitter). Anyway, when an ActionView is instantiated it eventually calls an important method named assign with a payload called assigns:

def assign(new_assigns) # :nodoc:
  @_assigns = new_assigns.each do |key, value|
    instance_variable_set("@#{key}", value)
  end
end
https://github.com/rails/rails/blob/d2cdf0be675b44771f950697fc0b19ef0ea453f9/actionview/lib/action_view/base.rb#L206

The ActionView::Base assign method is responsible for taking the new_assigns argument iterating through it to define instance variables using the instance_variable_set method. These instance variables are named, assigned and set in the template object using the key and value pairs in the new_assigns hash. This means that the template object rendered will have access to these variables immediately after being instantiated. Does this look familiar?

<% @my_index_var.each do |i| %>
  <p> Number: <%= I %> </p>
<% end %>
app/views/my_controller/index.html.erb

Ok so that all makes sense but how and where does the new_assigns payload come from?

ActionView Take Two

In the same ActionView gem lives a module called ActionView::Rendering where a view's "context" is built before it is rendered to the screen. In specific, the ActionView::Rendering module has a public hook method called render_to_body which is invoked by ActionController when the render method is called. Under the hood render_to_body calls a private method _render_template:

 def _render_template(options)
    variant = options.delete(:variant)
    assigns = options.delete(:assigns)
    
    context = view_context

    context.assign assigns if assigns
    lookup_context.variants = variant if variant

    rendered_template = context.in_rendering_context(options) do |renderer|
      renderer.render_to_object(context, options)
    end

    rendered_format = rendered_template.format || lookup_context.formats.first
    @rendered_format = Template::Types[rendered_format]

    rendered_template.body
end
https://github.com/rails/rails/blob/d2cdf0be675b44771f950697fc0b19ef0ea453f9/actionview/lib/action_view/rendering.rb#L108

Now, _render_template does quite a number of things but the important line for us to close in on is the invocation of the method view_context.

The view_context method is the glue that wraps everything together here:

 def view_context
  view_context_class.new(lookup_context, view_assigns, self)
end
https://github.com/rails/rails/blob/d2cdf0be675b44771f950697fc0b19ef0ea453f9/actionview/lib/action_view/rendering.rb#L92

It instantiates a new ActionView::Base object (view_context_class) and passes it an assigns hash consisting of... a call to the view_assigns method from AbstractController::Base !!

This means that a hash of the currently defined instance variables for the controller that called render will be passed along as an argument to the ActionView::Base initialize method. Nice! We've found the source. Here is the the above words in image form:

Phew... quite the journey but we've pulled  the curtains back on one of the more opaque parts of the Ruby on Rails framework and have made it through in one piece.

Have fun!