Should I use instance variables in Rails views?
In Ruby, an instance variable has a name starting with the @
character and is available only within the object's scope. In other words, it is accessible by all object's methods, without the need to pass it explicitly, but it is not shared between the objects.
Instance variables in Rails
Rails extends this idea and makes it look like instance variables are 'available' between some objects.
In the code below there are three instance variables initialized in the controller: @post
, @comments
, and @author
, that are called in the show
view template, and two partials rendered in that template.
# controllers/post_controller.rb
class PostsController < ApplicationController
def show
@post = Post.find(params[:id])
@author = 'John Doe'
@comments = @post.comments
end
end
<!-- views/posts/show.html.erb -->
<%= render "shared/author" %>
<p>
<strong>Title:</strong>
<%= @post.title %>
</p>
<p>
<strong>Body:</strong>
<%= @post.body %>
</p>
<%= render "shared/comments" %>
<!-- views/shared/author.html.erb -->
<h4>Written by: <%= @author %></h4>
<!-- views/shared/comments.html.erb -->
<h3>Comments</h3>
<% @comments.each do |comment| %>
<p>
<%= comment.body %>
</p>
<% end %>
Controller’s instance variables are copied to the view initializer, so they are available as the Action View's object's own instance variables. When the view is rendered, the instance variables used in the template are resolved and we are getting an HTML with all the data we wanted to present in it.
The view is not accessing controllers instance variables - it is using its own. That means, that no Ruby rules are bent or broken, and there are simply two objects sharing an access to the same objects via their instance variables.
Making instance variables accessible inside the whole view templates (including partials) is making the development process easier (because you do not have to worry about passing them around), but also can lead to some troubles and confusion in further development.
Potential issues
Global access
As you can see in the code above, an instance variable can be accessed anywhere in the view - @author
is called inside the _author.html.erb
partial, even though it is not used in the show.html.erb
.
It is also possible to create such a variable in the partial (or any other part of the view) or even modify the existing one, which is obviously not a good practice. The following code is valid from a semantic point of view but can cause quite a lot of confusion. It is changing the value of @post_title
, so in the show page instead of John Doe
we will get Foo
.
<!-- views/shared/author.html.erb -->
<h4>Written by: <%= @author %></h4>
<% @post.title = 'Foo' %>
Instance variables in views behave just like the global variables - they can be accessed and modified in multiple places, so it is easy to lose a track of what is going on in the view when the project grows.
Unusable partials
Let's assume, that we want to add a new feature that will be displaying the comment's author. We already have the partial with author’s details, so we can use it.
<!-- views/shared/comments.html.erb -->
<h3>Comments</h3>
<% @comments.each do |comment| %>
<p>
<%= comment.body %>
</p>
<%= render "shared/author" %>
<% end %>
But using it in the current form will not get us the result we want, because it will render details of the article's author.
Fixing partials
To use partials in multiple places, we should use local variables, that are passed to the partial explicitly. Our example will look like this:
<!-- views/posts/show.html.erb -->
<%= render "shared/author", locals: {author: @author} %>
<p>
<strong>Title:</strong>
<%= @post.title %>
</p>
<p>
<strong>Body:</strong>
<%= @post.body %>
</p>
<%= render "shared/comments", locals: {comments: @comments} %>
<!-- views/shared/author.html.erb -->
<h4>Written by: <%= author %></h4>
<!-- views/shared/comments.html.erb -->
<h3>Comments</h3>
<% comments.each do |comment| %>
<p>
<%= comment.body %>
<%= render "shared/author", locals: {author: comment.author} %>
</p>
<% end %>
Now the same partial is rendered in multiple places: on the top of the page - where it displays the name of the article's author, and after each comment displaying that comment's author.
This approach resolves also the issue with the global scope of instance variables because now they are still available everywhere, but they are used only on the show view page.
Taking it further
Some developers apply this approach not only to partials, but also to the whole views. In such a case controllers use explicit render
method with local variables, so our example would look like this:
# controllers/post_controller.rb
class PostsController < ApplicationController
def show
@post = Post.find(params[:id])
@author = 'John Doe'
@comments = @post.comments
render :show, locals: { post: @post, author: @author, comments: @comments }
end
end
<!-- views/posts/show.html.erb -->
<%= render "shared/author", locals: {author: author} %>
<p>
<strong>Title:</strong>
<%= post.title %>
</p>
<p>
<strong>Body:</strong>
<%= post.body %>
</p>
<%= render "shared/comments", locals: {comments: comments} %>
<!-- views/shared/author.html.erb -->
<h4>Written by: <%= author %></h4>
<!-- views/shared/comments.html.erb -->
<h3>Comments</h3>
<% comments.each do |comment| %>
<p>
<%= comment.body %>
<%= render "shared/author", locals: {author: comment.author} %>
</p>
<% end %>
As we already know all instance variables from the controller are passed to the view. Sometimes it may be hard to track all of them because they can be initialized not only in the controller action itself but also in multiple other places like filters, controller methods or even helpers. Using local variables in such cases may help to understand what is passed to the view, especially when the application grows.
Wrap up
Instance variables inside view templates behave like global variables. Using local variables for partials rendering solves most of the issues caused by it, and makes partials reusable. Some developers prefer not to use instance variables at all, and render views with locals from controllers.