November 19, 20244 min read

Understanding Rails View Rendering: Behind the ERB Magic

Ruby on RailsActionViewERBRuby

Ruby on Rails encapsulates a lot of complexity, allowing developers to focus on writing features without worrying about its internal. For the longest time, I wanted to understand what exactly happens when render is invoked in a Rails controller. Specifically, I was curious about how Rails parses ERB tags (<% %>, <%= %>) in the view. This blog post is based on a recent talk I gave at my local Ruby meetup (September 2024 meeting · Luma).

This blog assumes the reader has a basic understanding of the Rails framework. If you're new to Rails, I recommend reading this guide to get familiar with the basics.

For a contrived example

Let's say we have a UsersController and its associated index view template.

class UsersController < ApplicationController
  def index
    @user_email = "shard@gmail.com"
  end
end

# app/views/users/index.html.erb
#  instance view object
"

Welcome <%= @user_email %> to the Landing Page

"

Views are rendered in the context of the ActionView::Base class.

The Rendering Flow

Let's visualize the rendering process before diving into the details:

  1. Controller Action called
  2. Rails finds the ERB template
  3. Template compiled to Ruby method
  4. Method executes, generates HTML

The Transformation Process

How does rails transform the HTML source code in the view template?

From:
"<h1>Welcome <%= @user_email %> to the Landing Page</h1>"

To:
"<h1>Welcome shard@gmail.com to the Landing Page</h1>"

After exploring both the ActionPack and ActionView modules, which are the primary components responsible for handling requests, responses, and template rendering. Here's the breakdown of the rendering process that happens behind the scenes.

The Rendering Process

When def index action is called in a controller:

render is invoked implicitly in AbstractController::Rendering:23:

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

1. _normalize_render (after Controller Action called):

This method creates an options hash to provide context on the view template location and which view to render.

options = { template: 'index', prefixes: ["users", "application"] }

2. render_to_body (find template):

This method handles the actual view template rendering. Although render_to_body is called in several modules within ActionController and AbstractController, the render_to_body in ActionView::Rendering:105 is the key entry point for view rendering.

From there, ActionView looks up the template using the LookupContext class, parses the template path using the PathParser, and returns the extracted values in a hash.

path = 'app/views/users/index.html.erb' 
details = {
 :prefix=>"/Users/shardly/Documents/Main/Pet_Projects/shoe_stride/app/views/static_pages", 
 :action=>"index", 
 :partial=>false, 
 :locale=>nil, 
 :handler=>:erb, 
 :format=>:html, 
 :variant=>nil
}

After some additional steps, an instance of the TemplateRenderer class creates an instance of the Template class, passing arguments like :handler(erb, haml), :format(HTML, js, etc), :variant (mobile, desktop) and a couple more.

3. Compiled to Ruby method:

Next, the compile method in the Template class is invoked, dynamically creating an instance method. Based on the HTML source code, the generated method would look like the example shown below.

source = def _app_views_users_index_html_erb___3847165856814041552_19860(local_assigns, output_buffer)
            @virtual_path = "users/index"
            @output_buffer.safe_append='

Welcome '.freeze @output_buffer.append=( @user_email ) @output_buffer.safe_append=' to the Landing Page

'.freeze; @output_buffer.to_s end

The @output_buffer is an instance of the OutputBuffer class in the ActionView module. It is responsible for accumulating the rendered HTML content in Rails views. It is vital to manage how Ruby expressions, helpers, and templates are processed and outputted as complete HTML responses. It also ensures efficient rendering and proper HTML escaping.

The new instance method (`_app_views_users…`) is then added to the <ActionView::Base:0x00000000009bf0> class through module_eval:

# mod = 
mod.module_eval(source, identifier, 0)

4. Generate html:

Finally, def _run is invoked on the ActionView::Base object, which in turn calls public_send, invoking the dynamically created method:

# method = "_app_views_users_index_html_erb___3847165856814041552_19860"

def _run(method, template, locals, buffer, add_to_stack: true, &block)
  _old_output_buffer, _old_virtual_path, _old_template = @output_buffer, @virtual_path, @current_template
  @current_template = template if add_to_stack
  @output_buffer = buffer
  public_send(method, locals, buffer, &block)

public_send will return:

"

Welcome shard@gmail.com to the Landing Page

"

That's it!

Wrapping Up

A key takeaway here is how Rails dynamically creates and evaluates methods during template rendering. Ruby allows you to use module_eval to define methods on the fly on a class, such as in this example:

class Post
end

post = Post.new

Post.module_eval do
  def comment
    'my comment'
  end
end

post.comment #=> "my comment"

Rails modules come with built-in debugging tools like debugger, which made this research much easier. You can open a module in your preferred IDE by running:

$ sample_rails_project git:(main) > EDITOR=code bundle open actionview
$ sample_rails_project git:(main) > EDITOR=code bundle open actionpack

If you make any changes while exploring, you can reset the files to their original state by running:

$ sample_rails_project git:(main) > gem pristine actionview
$ sample_rails_project git:(main) > gem pristine actionpack

Conclusion

Exploring Rails' source code has deepened my appreciation for the framework, and I learned some interesting Ruby concepts along the way. The framework extensively follows the SOLID principles, with the Single Responsibility Principle being particularly evident in many of its classes. Through this research, I also discovered some text documentation that needs to be updated in the ActionPack module, for which I have submitted a pull request. I hope this blog post empowers you to explore the underlying mechanics of the tools you're using.

Go out and explore yourself it is a rewarding experience.

Thanks for reading!