What Happens When You Call render? Let's Understand the Rails Rendering Process
This article explains the Rails rendering process in the context of returning JSON data from the controller. Hopefully, it will make it clear what really happens when you call the render method from the controller.
This post assumes an understanding of metaprogramming concepts in Ruby. If you aren't comfortable with it, an excellent place to start is by reading the book Metaprogramming Ruby 2.
Ruby on Rails makes it very easy to render JSON data from the controller using the render method.
Ever wondered what really goes on behind the scenes? How does the render method convert the list of tasks to JSON format and sends it in the response? How Rails creates the renderers? This article will try to explain how rendering works in Rails.
By the end of the article, you should have a much better understanding of what really happens when you call the render method from the controller.
Let's start our journey into the rendering process by considering the Rails controller hierarchy.
Your typical Rails controllers inherit from the ApplicationController class which itself inherits from the ActionController::Base class. This class includes the AbstractController::Rendering module, providing the render method. Hence the render method becomes available in all your controllers.
When you call render from the PostsController, Rails calls the AbstractController::Rendering#render method and sets its result on the response.
class PostsController < ApplicationController
def show
@post = Post.first
render json: @post
end
end
# actionpack/lib/abstract_controller/rendering.rb
module AbstractController
module Rendering
def render(*args, &block)
options = _normalize_render(*args, &block)
rendered_body = render_to_body(options)
self.response_body = rendered_body
end
end
end
The normalize_render method creates the hash of options containing the passed data @posts along with the template and the layout. It looks like this:
options = {
:json=>#<Post:0x00000001145d0470>,
:template=> ..,
:layout=> ..
}Ignore the template and layout keys for now. The important point is that the options[:json] contains the post object we passed in the controller.
Now let's move to the next line in the render method, which calls the render_to_body method, passing the options hash. This method resides in the action_controller/metal/renderers.rb file.
# action_controller/metal/renderers.rb
module ActionController
module Renderers
def render_to_body(options)
_render_to_body_with_renderer(options) || super
end
end
end
The _render_to_body_with_renderer method loops over a set of pre-defined renderers and checks if it includes the renderer we need (:json in our case). If it does, then it dynamically generates and calls a method named method_name using the Object#send method.
# action_controller/metal/renderers.rb
module ActionController
module Renderers
def _render_to_body_with_renderer(options)
_renderers.each do |name|
if options.key?(name)
_process_options(options)
method_name = Renderers._render_with_renderer_method_name(name)
return send(method_name, options.delete(name), options)
end
end
nil
end
end
end
To understand this method, we need two pieces of information:
- How renderers are initialized in the
_renderersvariable. - How
method_nameis generated and what it does when called.
The _renderers variable
It's a Set containing renderer names that correspond to available renderers. Default renderers are json, js, and xml.
This variable is defined in the Renderers::All concern. As soon as it's included in the Base class, it includes the ActionController::Renderers module. This module sets up the RENDERERS variable to an empty set and then calls the add method which fills this set.
# action_controller/metal/renderers.rb
module ActionController
module Renderers
RENDERERS = Set.new
module All
extend ActiveSupport::Concern
include Renderers
included do
self._renderers = RENDERERS
end
end
add :json do |json, options|
json.to_json(options) # simplified
end
add :xml { ... }
add :js { ... }
end
end
The add method takes the name of the renderer (:json) and a block, and
- Dynamically defines a method that will execute the block later.
- Add the name of the renderer to
RENDERERS, a set of renderer names.
# action_controller/metal/renderers.rb
module ActionController
module Renderers
def self.add(key, &block)
define_method(_render_with_renderer_method_name(key), &block)
RENDERERS << key.to_sym
end
end
end
Finally, the RENDERERS variable contains this array: [:json, :js, :xml] which gets assigned to the _renderers variable.
This brings us to the next point, that is, how the method named method_name is generated.
How method_name is generated
Let's inspect the above add method in detail. It defines a method dynamically using the Module#define_method and passes it a name which it generates using the _render_with_renderer_method_name helper.
def self._render_with_renderer_method_name(key)
"_render_with_renderer_#{key}"
end
Passing :json will return the string _render_with_renderer_json. This is the name of the dynamic method. Its body is the block we passed to the add method.
To summarize, calling add :json as above defines the following method:
def _render_with_renderer_json(json, options)
json.to_json(options) # simplified
end
And this gives us the final piece of the puzzle: What the method defined by method_name does when called. It simply calls to_json on the object you passed to the render method and returns it.
To generalize, it renders the given object, depending on the renderer.
Let's unwind the stack and go back to where we started, the render_to_body_with_renderer method.
# action_controller/metal/renderers.rb
module ActionController
module Renderers
def _render_to_body_with_renderer(options)
_renderers.each do |name|
if options.key?(name)
_process_options(options)
method_name = Renderers._render_with_renderer_method_name(name)
return send(method_name, options.delete(name), options)
end
end
nil
end
end
end
As we saw earlier, the options hash looks like this:
options = {
:json=>#<Post:0x00000001145d0470>,
:template=> ..,
:layout=> ..
}Since options contains the :json renderer, the control enters the if conditional block and calls the dynamically generated _render_with_renderer_json method. Additionally, it passes the value corresponding to the :json key, which is an instance of the Post class.
Let's revisit the dynamically generated _render_with_renderer_json method body again.
def _render_with_renderer_json(json, options)
json.to_json(options)
end
It will call post.to_json and return the following output.
"{\"title\":\"hello world\"}"Let's unwind the stack once more and return to the render method we saw at the beginning.
# abstract_controller/rendering.rb (simplified)
def render(*args, &block)
options = _normalize_render(*args, &block)
rendered_body = render_to_body(options)
self.response_body = rendered_body
end
The above JSON string is assigned to the response_body on the controller, which is ultimately sent to the client.
If you're curious how response_body actually works, keep on reading.
Sending Response
Let's inspect the Rails controller hierarchy once again.
Ultimately, your PostsController class inherits from the ActionController::Metal class. To learn more about the metal controller, check out the following article: Metal Controller in Rails
The ActionController::Metal class provides the setter for the response_body, which assigns the value to the response body.
# action_controller/metal.rb
module ActionController
class Metal
def response_body=(body)
body = [body] unless body.nil? || body.respond_to?(:each)
if body
response.body = body
super
else
response.reset_body!
end
end
end
end
This whole request lifecycle was part of the dispatch method. Once the action is processed, the dispatch method calls to_a method, returning the response.to_a result.
# action_controller/metal.rb
module ActionController
class Metal
def dispatch(name, request, response)
set_request!(request)
set_response!(response)
process(name)
request.commit_flash
to_a
end
def to_a
response.to_a
end
end
end
The to_a method on the Response calls the rack_response method, which returns the final result, the Rack-compatible array of the status, headers, and body. To learn more about Rack, check out the following article: The Definitive Guide to Rack for Rails Developers
# action_dispatch/http/response.rb
module ActionDispatch
class Response
def to_a
rack_response @status, @header.to_hash
end
def rack_response(status, header)
[status, header, RackBody.new(self)]
end
end
end
Which ultimately returns the following JSON you see in the browser.
Conclusion
Whenever we invoke the following method in our application
render json: @post
it invokes the block associated with the :json renderer.
add :json do |json, options|
json.to_json(options) # simplified version
end
The local variable json inside the block points to the @post object, and the other options passed to render will be available in the options variable. The block returns the JSON representation of the post and sends it to the client.
And that's what happens when you call render json: @post from your controller.
Sign up for my newsletter
Let's learn to become better developers.