Rails: パーシャルでstrict locals
マジックコメントを使うときの注意点(翻訳)
Railsのパーシャル(partial)は随分前から存在していますが、オブジェクト構造に支えられていない単なるERBスニペットに過ぎないので、扱いにくい場合があります。
近年のViewComponentやPhlexなどのライブラリは、ビューテンプレートに意味論的な構造を追加することで、ビューレイヤの改善を試みています。これらは優秀なライブラリであり、私個人も、自分が取り組んでいるほぼすべてのプロジェクトもViewComponentを使っています。
とは言うものの、Railsの控えめなパーシャルライブラリも捨てたものではありません。今でも多くのユースケースで優れた機能を発揮できる力があると感じています。
Railsチームは絶え間なく機能の改善に取り組んでいます。そこで今回は、Rails 7.1から導入された新機能であるstrict locals
(厳密なlocals
)を見てみましょう。
🔗 Railsのパーシャルとローカル変数
Railsのパーシャルにどんなローカル変数をいくつ渡しても、パーシャル内で魔法のようにそれらのローカル変数が使えるようになります。
<%# app/views/application/_badge.html.erb %>
<ui-badge>
<p>
<%= title %>
<%= tag.i class: icon %>
</p>
</ui-badge>
しかしこれは理想的とはいえません。マークアップ内のブロックのコードが増えてくると、パーシャルが実際にはどの変数を受け取るのかを確認するのが難しくなるからです。
さらに、変数は動的に作成されるので、渡し損なった変数があった場合はnil
にならず、エラーになってしまいます。
たとえば、上のパーシャルのicon
という変数がオプショナル(省略可能)で、パーシャルに渡さなくてもよいものだとしましょう。
<%# app/views/application/_badge.html.erb %>
<ui-badge>
<p>
<%= title %>
<%= tag.i class: icon if icon.present? %>
</p>
</ui-badge>
このパーシャルを以下のようにicon
を渡さずにレンダリングします。
<%= render "application/badge", title: "New" %>
するとActionView::Template::Error
がraiseされ、undefined local variable or method 'icon' for #<ActionView::Base:0x00000000023078>)
というメッセージが表示されます。
コードを読む人にとって、icon
がオプショナルであるということはわかりにくくなっています。
Rails 7.1より前までのソリューションは、以下のようにlocal_assigns
ハッシュを調べることで、注入されたローカル変数を全部検査するという方法でした。
<%# app/views/application/_badge.html.erb %>
<% icon ||= local_assigns[:icon] %>
<ui-badge>
<p>
<%= title %>
<%= tag.i class: icon if icon.present? %>
</p>
</ui-badge>
こうすることで、icon
変数が常に設定済みになるので、上のエラーは修正されます。
Rails 7.1からは、strict locals
という概念を導入することで、パーシャル内のローカル変数の扱いにおける欠点への対処を試みています。
🔗 Railsのstrict locals
について
Rails 7.1以降は、以下のようにパーシャルの冒頭1に<%# locals: %>
というマジックコメントを書くことで、パーシャルのローカル変数とそのデフォルト値を定義できるようになりました。
<%# 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はこのマジックコメントを解析して、定義済みの変数をパーシャルファイルに設定します。これで、コードを読むときに、パーシャルに渡すべき変数とデフォルト値が一目瞭然になります。
このパーシャルは上述のようにレンダリングされるはずです。試しに以下のように呼び出し側のtitle
変数を削除して、何が起こるかを確認してみてください。
<%= render "application/badge" %>
ArgumentError
エラーが発生しますが、従来よりわかりやすいmissing local: :title
というメッセージが表示されます。
🔗 strict locals
はどんなときに使うべきか
strict locals
は必ずしも扱いやすいとは限りません。特にレガシーアプリで無理して使うのはよくないことがあります。
strict locals
が最も適しているユースケースを見てみましょう。
カードにユーザーの詳細を表示する以下のパーシャルがあるとします。ただし、「最後にサインインした日時」を表示してよいのは管理者だけです。
<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>
このパーシャルはたくさんの変数を受け取り、しかも省略可能なオプショナル変数もあるため、strict locals
に最適です。
以下のマジックコメントをファイルに追加すれば、パーシャルに渡すべき変数がとてもわかりやすくなります。
<%# locals: (name:, email:, last_signed_in: nil) %>
モデルに関連付けられるパーシャル(posts/_post.html.erb
)の場合、この種のパーシャルに渡すのはpost
変数1つだけであることは見え見えです。
私は、こういう場合にわざわざstrict locals
を使うのは必ずしもよいとは言えないと思います。
🔗 strict locals
の落とし穴
strict locals
にはそれなりにハマりポイントがあるので、アプリ全体で一律に使うのはおすすめしません。
🔗 1: コレクションを暗黙でレンダリングする場合
以下のように、レンダリングする対象が暗黙でコレクションになっている場合を考えてみましょう。
<%= render @posts %>
この場合のRailsは、post
変数の他に2つの変数も_post
パーシャルに渡すので、注意が必要です。
Rails 7.1.2より前は、エラー防止のために以下を手動で定義しなければなりませんでした。
<%# app/views/posts/_post.html.erb %>
<%# locals: (post:, post_counter:, post_iteration:) %>
<%# ... %>
この問題はRails 7.1.2で修正され(#49782)、これらのコレクション用追加変数は省略可能になりました(ただしこれらの変数が存在していることは知っておいても損はありません)。
🔗 2: Action Cableでブロードキャストする場合
Active Recordのコールバックで、以下のようにパーシャルをAction Cable経由でブロードキャストする場合を考えてみましょう。
class Comment < ApplicationRecord
after_create_commit -> {
broadcast_append_later_to(
post,
target: dom_id(post, :comments)
)
}
end
この場合、request_id
というローカル変数を指定して、デフォルト値をnil
にしておく必要があります。これらの値はRails内部で利用されるからです。
<%# app/views/comments/_comment.html.erb %>
<%# locals: (comment:, request_id:) %>
<%# ... %>
こうしておかないと、コールバックがトリガーされた瞬間にエラーになります。
以上でstrict locals
探索の小旅行はおしまいです。
🔗 その他に知っておいて欲しいこと
既存のパーシャルにstrict locals
を追加することは、指定のファイルに対する「すべてかゼロか」という命題であり、その中間はありません。マジックコメントに書かれている変数は、段階的に追加できなくなります。上記の落とし穴で示したように、マジックコメントで宣言しなければならない変数をすべて事前に確定するのは難しい場合があります。
strict locals
を使えば、コードについての推論がはるかに楽になるので、新しいアプリで広く利用する(あるいはレガシーアプリで遡って段階的に追加する)価値はあると思います。
レガシーアプリで重要なのは、strict locals
を追加したパーシャルをテストスイートでしっかりカバーしておくことです。変数を渡し忘れると例外が発生するので、変更をデプロイする前にすべてのユースケースをしっかりテストしておく必要があります。
私は、コメントに機能を持たせるべきではないと信じているので、「マジックコメント」方式の大ファンというほどではありませんが、それでもstrict locals
はRailsに追加された素晴らしい機能だと思っています。これまでパーシャルにローカル変数を割り当てる作業はいつも面倒でしたが、この機能のおかげで楽になります。ただし落とし穴にはどうかご用心!
関連記事
- 訳注: マジックコメントを書く場所はパーシャルの冒頭でなくても、途中や末尾などでも有効です。 ↩
概要
原著者の許諾を得て翻訳・公開いたします。
参考: §5.8 厳密な
locals
-- Action View の概要 - Railsガイド日本語タイトルは内容に即したものにしました。