Rails: パーシャルとヘルパーからViewComponentに乗り換える手順(翻訳)
Railsによるビュー層への回答は、歴史的には「ビュー(view)」「パーシャル(partial、部分テンプレートとも)」「ヘルパー(helper)」のミックスでした。
実際、Railsアプリがシンプルな間は、これで十分です。
本記事をお読みの皆さんの中には、Railsアプリで大量に溢れかえっているパーシャルやヘルパーがつらくてたまらない方や、そんな苦労を二度と味わわないためにViewComponentへの移行を既に検討している方もいるでしょう。
どちらにしろ、現状のRailsアプリには既にかなりの(下手をすると大量の)パーシャルが存在していて、そこから多数のヘルパーが呼び出されていることでしょう。
ViewComponentを使うべき理由については以下の記事に書きました。ViewComponentを使うメリットとデメリットや、スロットなどの高度な機能についても一通り触れています。
さて、パーシャルからViewComponentに乗り換えるには、どんな手順で進めるのがよいでしょうか?
🔗 最初は1個のパーシャルを置き換えてみよう
アプリの規模によっては、既にパーシャルが相当たくさん使われている場合もあるでしょう。これらをViewComponentに置き換えようとすると結構な工数がかかるかもしれません。
別記事でも書いたように、ViewComponentを使うと確実に速度が向上する(テストの速度も)ので、置き換えが進むに連れて自分やチームの仕事がはかどることを実感できるようになるでしょう。
しかしアプリの規模にかかわらず、最初は1個のパーシャルを置き換えて、そこでいろいろ試してみましょう。
置き換えるパーシャルは、以下のようなものが理想的です。
- ヘルパーメソッドを使っている
- コレクションを使っている
理由は、ヘルパーに置くのは「グローバルなもの」だけにしておきたいからです(私のRails Designerで提供しているビューヘルパーもグローバルなものだけが置かれています)。
また、コレクションではそれなりの処理を行うことになるので、お試しの場として理想的です。
🔗 例
能書きはこれくらいにして、サンプルコードを見てみましょう。
<div class="messages-list">
<h2>Messages (<%= @messages.count %>)</h2>
<ul class="flex flex-col gap-y-3">
<% @messages.each do |message| %>
<li class="message-item">
<div class="flex items-center justify-between">
<strong><%= message.sender.name %></strong>
<small><%= format_message_timestamp(message.created_at) %></small>
</div>
<p><%= truncate_message_body(message.body) %></p>
<div class="flex items-center justify-between">
<%= message_status_badge(message.status) %>
<%= link_to "View", message_path(message), class: "btn btn--primary" %>
</div>
</li>
<% end %>
</ul>
</div>
メッセージのリストを表示する、Railsパーシャルの典型的なコードです。以下のヘルパーが使われていることもわかります。
module MessagesHelper
def format_message_timestamp(timestamp)
timestamp.strftime("%b %d, %Y at %I:%M %p")
end
def truncate_message_body(body)
truncate(body, length: 100, separator: " ")
end
def message_status_badge(status)
case status
when "read"
content_tag(:span, "Read", class: "badge badge-success")
when "unread"
content_tag(:span, "Unread", class: "badge badge-warning")
else
content_tag(:span, "Unknown", class: "badge badge-secondary")
end
end
end
ViewComponentの嬉しい点は、作業を楽に行えることです。
最初にコンポーネントのRubyファイルを1つ作成しましょう。ViewComponentが既に追加済みである前提で、bin/rails g component MessagesListコマンドを実行します。
デフォルトでは、以下の2つのファイルが作成されます。
- app/components/messages_list_component.rb
- app/components/messages_list_component.html.erb
前者は、ヘルパーのすべてのRubyメソッドの引っ越し先です(MessagesHelpersのformat_message_timestampやmessage_status_badgeなど)。
後者はERBコードの引っ越し先です。
最初にapp/components/messages_list_component.rbをセットアップしましょう。
class MessagesListComponent < ViewComponent::Base
def initialize(message:)
@message = message
end
end
app/components/messages_list_component.html.erbは以下のようになります。
<li class="message-item">
<div class="flex items-center justify-between">
<strong><%= @message.sender.name %></strong>
<small><%= format_message_timestamp(@message.created_at) %></small>
</div>
<p><%= truncate_message_body(@message.body) %></p>
<div class="flex items-center justify-between">
<%= message_status_badge(@message.status) %>
<%= link_to "View", message_path(@message), class: "btn btn--primary" %>
</div>
</li>
引っ越す前のパーシャルと同じような感じになっていますね。まさにコピペで済む作業です。
このMessagesListComponentをビューでレンダリングするには、以下のように書きます。
<div class="messages-list">
<h2>Messages (<%= @messages.count %>)</h2>
<ul class="flex flex-col gap-y-3">
<%= render(MessagesListComponent.with_collection(@messages)) %>
</ul>
</div>
ここではViewComponentのコレクション機能を使っています。
次はヘルパーの引っ越しです。
引っ越し元のMessagesHelperモジュールは、Messagesビューだけではなくすべてのビューからアクセス可能になってしまいます。truncate_message_bodyヘルパーはアプリのどのビューからも呼び出せるので、誤って呼び出してしまうと厄介なバグが生じる可能性があります。
それではMessagesHelperのメソッドをすべてMessagesListComponentに移動し、元のヘルパーファイルを削除しましょう!🗑️
class MessagesListComponent < ViewComponent::Base
def initialize(message:)
@message = message
end
def timestamp
@message.created_at.strftime("%b %d, %Y at %I:%M %p")
end
def truncated_body
truncate(@message.body, length: 100, separator: ' ')
end
def status_badge
case @message.status
when "read"
content_tag(:span, "Read", class: "badge badge-success")
when "unread"
content_tag(:span, "Unread", class: "badge badge-warning")
else
content_tag(:span, "Unknown", class: "badge badge-secondary")
end
end
end
ヘルパーからコンポーネントへの引っ越しが終わったら、app/components/messages_list_component.html.erbも以下のように更新しましょう。
<li class="message-item">
<div class="flex items-center justify-between">
<strong><%= @message.sender.name %></strong>
<small><%= timestamp %></small>
</div>
<p><%= truncated_body %></p>
<div class="flex items-center justify-between">
<%= status_badge %>
<%= link_to "View", message_path(@message), class: "btn btn--primary" %>
</div>
</li>
これで引っ越し完了です。ついにViewComponentの最初のコンポーネントができました🌟。今までのようなグローバルなヘルパーメソッドではなく、コンポーネント専用のメソッドを安心して使えるようになりました。
🔗 ViewComponentは無理に使うよりも、新しいUI要素コンポーネントだけで使おう
ViewComponentの最初のコンポーネントでいい感触をつかんだら、プルリクを作成してチームと共有し、「改良の余地はあるか」「懸念事項はないか」などの意見を聞いてみましょう。
一般論として、新しい技術を導入するときは、決定方法についてのドキュメントをできるだけみっちり書くのがよい方法です。本ブログのシリーズ記事のような参考となる良記事へのリンクも盛り込んでおきましょう。
チームの了承を得たとしても、全パーシャルをViewComponentに移行しようとしないことが大事です。実際問題、すべてのパーシャルをViewComponent化するのがよいとは限りません。
パーシャルをViewComponentに移行するべきタイミングは、以下の通りです。
- パーシャルから呼び出しているヘルパーメソッドの利用範囲が局所的で、使い回しが効かない
- パーシャルに多数の変数が渡されている
- パーシャルで堅固なテストが必要(管理者向け機能の制御など)
たとえば、私のアプリのほとんどでは、views/shared/_head.html.erbというパーシャルも使われています。これは全レイアウト共通の典型的な
チームメンバーがViewComponentに慣れてきたら、既存のパーシャルを少しずつViewComponentに置き換えていくのが賢明です。プルリクのサイズが大きくならないようにしておきましょう。
ViewComponentへの移行支援が必要な方はこちらまでメールください。
概要
元サイトの許諾を得て翻訳・公開いたします。
日本語タイトルは内容に即したものにしました。
参考: Action View の概要 - Railsガイド