Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rails: パーシャルでstrict `locals`マジックコメントを使うときの注意点(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

参考: §5.8 厳密なlocals -- Action View の概要 - Railsガイド

日本語タイトルは内容に即したものにしました。

Rails: パーシャルでstrict localsマジックコメントを使うときの注意点(翻訳)

Railsのパーシャル(partial)は随分前から存在していますが、オブジェクト構造に支えられていない単なるERBスニペットに過ぎないので、扱いにくい場合があります。

近年のViewComponentPhlexなどのライブラリは、ビューテンプレートに意味論的な構造を追加することで、ビューレイヤの改善を試みています。これらは優秀なライブラリであり、私個人も、自分が取り組んでいるほぼすべてのプロジェクトも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に追加された素晴らしい機能だと思っています。これまでパーシャルにローカル変数を割り当てる作業はいつも面倒でしたが、この機能のおかげで楽になります。ただし落とし穴にはどうかご用心!

関連記事

Rails: ビューのパーシャルではローカル変数だけを使うこと(翻訳)

Rails tips: ビューのcontent_tagのあまり知られていないオプション(翻訳)


  1. 訳注: マジックコメントを書く場所はパーシャルの冒頭でなくても、途中や末尾などでも有効です。 

CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。