ruby

Ruby on Rails 7.1: Partial Strict Locals and Their Gotchas

Ayush Newatia

Ayush Newatia on

Ruby on Rails 7.1: Partial Strict Locals and Their Gotchas

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.

erb
<%# 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:

erb
<%# app/views/application/_badge.html.erb %> <ui-badge> <p> <%= title %> <%= tag.i class: icon if icon.present? %> </p> </ui-badge>

We render it as:

erb
<%= 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.

erb
<%# 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.

erb
<%# 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:

erb
<%= 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:

erb
<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:

erb
<%# 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:

erb
<%= 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:

erb
<%# 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:

ruby
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.

erb
<%# 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!

Ayush Newatia

Ayush Newatia

Guest author Ayush is a freelance Ruby and Rails developer. He's the author of The Rails and Hotwire Codex and part of the Bridgetown core team. He also runs a privacy focused mailing list app called Scattergun.

All articles by Ayush Newatia

Become our next author!

Find out more

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps