Rails partials have been around for years, but they can be clunky since they're just ERB snippets without a backing object structure.
Recently, libraries like ViewComponent and Phlex have tried to improve the view layer by adding more semantic structure to the templates. These are great libraries and I personally reach for ViewComponent
on almost every project I work on. That said, I still feel the humble Rails partial still works great for many use cases.
The Rails team is always trying to improve its offering, so let's look at a new feature introduced in Rails 7.1: strict locals!
Rails Partials and Local Variables
We can pass in arbitrary local variables into Rails partials and they're magically available for use.
<%# app/views/application/_badge.html.erb %> <ui-badge> <p> <%= title %> <%= tag.i class: icon %> </p> </ui-badge>
This isn't ideal, because it's tricky to ascertain which variables a partial accepts if there's a large block of markup. Also, since variables are dynamically created, a missing variable won't return nil
but will cause an error.
Let's say the icon
was optional in the above partial:
<%# app/views/application/_badge.html.erb %> <ui-badge> <p> <%= title %> <%= tag.i class: icon if icon.present? %> </p> </ui-badge>
We render it as:
<%= render "application/badge", title: "New" %>
That will raise an ActionView::Template::Error
, with the message undefined local variable or method 'icon' for #<ActionView::Base:0x00000000023078>
.
The fact that icon
is optional is not obvious to a reader either. Until Rails 7.1, the solution was to inspect the local_assigns
hash which contains all injected local variables.
<%# app/views/application/_badge.html.erb %> <% icon ||= local_assigns[:icon] %> <ui-badge> <p> <%= title %> <%= tag.i class: icon if icon.present? %> </p> </ui-badge>
This fixes the above error because the icon
variable is now always set.
Rails 7.1 attempts to address the shortcomings of local variables in partials with the concept of strict locals.
What Are Strict Locals in Rails?
Starting in Rails 7.1, we can write a magic comment at the top of partials, which defines the local variables it accepts and any default values for them.
<%# app/views/application/_badge.html.erb %> <%# locals: (title:, icon: nil) %> <ui-badge> <p> <%= title %> <%= tag.i class: icon if icon.present? %> </p> </ui-badge>
Rails parses this comment and sets the defined variables in the partial. Now it's plainly obvious to a reader which variables the partial accepts and any default values.
The partial should still render as shown above. Try excluding the title
variable from the call site and see what happens:
<%= render "application/badge" %>
You get an ArgumentError
with the message missing local: :title
: a more descriptive error message than before!
When to Use Strict Locals
Strict locals can be fiddly, so it's not always a good idea to use them, especially in legacy apps.
Let's take a use case that's perfect for strict locals. Say we have a partial showing a user's details in a card, but only admins can see when they last signed in:
<ui-card> <dl> <dt>Name</dt> <dd><%= name %></dd> <dt>Email</dt> <dd><%= email %></dd> <% if last_signed_in %> <dt>Last signed in at</dt> <dd><%= last_signed_in %></dd> <% end %> </dl> </ui-card>
This partial takes several variables and has an optional variable, making it a perfect case for strict locals. Adding the following magic comment to the file makes it significantly easier to skim:
<%# locals: (name:, email:, last_signed_in: nil) %>
In the case of a model's associated partial (e.g. posts/_post.html.erb
), it's fairly obvious that such a partial accepts a single post
variable. I don't think strict locals are necessarily a good idea for these cases.
Strict Locals Gotchas
There are a couple of gotchas to be wary of, which is why I don't recommend using strict locals across the board.
Implicitly Rendering a Collection
When implicitly rendering a collection:
<%= render @posts %>
Rails injects the _post
partial with two variables in addition to the post
variable. Before Rails 7.1.2, these needed to be manually defined to prevent an error:
<%# app/views/posts/_post.html.erb %> <%# locals: (post:, post_counter:, post_iteration:) %> <%# ... %>
This problem was fixed in Rails 7.1.2 and these variables are now optional (but it's good to know they exist if needed)!
Broadcasting Over ActionCable
When broadcasting a partial over ActionCable from an ActiveRecord callback:
class Comment < ApplicationRecord after_create_commit -> { broadcast_append_later_to( post, target: dom_id(post, :comments) ) } end
You need to specify a request_id
local variable defaulting to nil
, as this is used under the hood by Rails.
<%# app/views/comments/_comment.html.erb %> <%# locals: (comment:, request_id:) %> <%# ... %>
Not doing so results in an error when the callback is triggered.
And that's our whistle-stop tour done!
A Few Things to Keep in Mind
Adding strict locals to existing partials is an all-or-nothing proposition for a given file. We can't incrementally add variables to a magic comment as we find them. As shown in the above gotchas, it can be tricky to ascertain all the variables that need to be declared in the magic comment.
Given how much easier it is to reason about code with strict locals, I think they're worth using extensively in new apps (and progressively adding retroactively to legacy apps).
It's important to ensure your test suite in legacy apps covers the partials to which you add strict locals. An omission will cause an exception, so you need to be sure you've tested all use cases before deploying changes.
While I'm not a huge fan of the magic comment approach (as I believe comments should never have functionality), I think strict locals are a great addition to Rails. It's always been fiddly to assign locals to a partial and these do make it easier. Just beware of the gotchas!
Wrapping Up
In this post, we took a quick look at strict locals in Rails 7.1. We saw how partials and local variables work before exploring when to use strict locals and some gotchas.
Happy coding!
P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!