Rails: パーシャルよりもViewComponentを選ぶべき理由(翻訳)
Reactにインスパイアされて生まれたViewComponentが紹介されたのは、RailsConf 2019でした。ViewComponentはデータフローを改善してビューをテストしやすくし、ビューのコードを美しく整えられます。
類似のソリューションはViewComponentの前後にもいくつか登場していましたが、その中から私はGitHubの支援を受けたViewComponentを選びました(私のRails Designerでもトップチョイスになっているほどです)。
ViewComponentを導入するうえで気がかりな点はあるでしょうか?Railsにはパーシャルもヘルパーも揃っているのに、わざわざ依存関係を増やす必要はあるのでしょうか?
では最初にViewComponentの長所と短所をリストアップしておきましょう。
長所
- コード編成が改善される
- パフォーマンスが向上する
- 拡張やコンポジションがはかどる
短所
- 依存関係が増える
- 「やりすぎ」のワナに陥りがち
- 学習コストが増える
🔗 パーシャルやヘルパーからViewComponentに乗り換える理由
パーシャルやヘルパーは、Railsアプリの中核となる機能であり、Rails開発者が長年使いこなして慣れ親しんでいます。
ここで申し上げておきたいのですが、パーシャルやヘルパーを使うこと自体は一向に構わないと思うのです。
実際、私はパーシャルをデフォルトで使っていますし、ViewComponentを使うのは必要なデータ処理が複雑になってきたときだけに限っています(Railsの規約に沿うならばヘルパーを使うのが普通ですし、さもなければより高度なDecoratorパターンやPresenterパターンを使うでしょう)。
Railsのビューヘルパーの最大の問題は、グローバルであることです。たとえばUserHelperでnameメソッドを定義すると、あらゆるビューやパーシャルから呼び出し可能になってしまいます。都合よくuserオブジェクトだけで呼び出し可能になってくれたりはしません1。ヘルパーメソッドの命名を工夫すればある程度しのげるかもしれませんが、それでも理想からはほど遠いしろものです。
もちろん私だってRailsのヘルパーは使っていますよ!以下のようにアプリ全体でグローバルに使えるメソッドなら、それに適したヘルパーに配置しています。
component "global_hotkeys"
(render GlobalHotKeysComponent.new呼び出しを置き換えるヘルパー)stream_notification "Saved"
(turbo_stream.replace "notification" { NotificationComponent.new(message: "Saved") }を置き換えるヘルパー)- その他の日付/時刻のフォーマット用ヘルパー(
custom_format(user.created_at)など)
詳しくは以下の記事をどうぞ。
参考: From Partials (and Helpers) to Embracing ViewComponent in Rails | Rails Designer
🔗 ViewComponentはパフォーマンスを改善する
ViewComponentは、パーシャルと比べて著しく高速です。主な理由は、ViewComponentのテンプレートはパーシャルのような実行時ではなく、アプリケーションの起動時にすべてプリコンパイルされるからです。ERBが多用されていればいるほど、速度が目に見えて改善されます。
ViewComponentのドキュメントによると、現実のユースケースでは最大でパーシャルの10倍2高速です。
また、コンポーネントのテストも高速です(Railsのビューのテストが伝統的に遅いのかもしれませんが)。
ただし、小〜中規模のRailsアプリでは実際にはそこまで速くならないことはここで触れておく必要があります。
🔗 ViewComponentのしくみ
コンポーネントは通常app/components/ディレクトリに配置され、以下のようなコードになります(ViewComponentドキュメントより抜粋)。
class MessageComponent < ViewComponent::Base
erb_template <<-ERB
<h1>Hello, !</h1>
ERB
def initialize(name:)
@name = name
end
end
このコンポーネントを以下のようにERBでインスタンス化して使います。
<%= render(MessageComponent.new(name: "World"))
テストコードは以下のような感じになります。
require "test_helper"
class MessageComponentTest < ViewComponent::TestCase
def test_render_component
render_inline(ExampleComponent.new(name: "Hello, World!"))
assert_text("Hello, World!")
end
end
🔗 高度なUIコンポーネント
ViewComponentそのものは単なるRubyオブジェクトなので、拡張(継承)やコンポジションで機能を追加できます。ビューのロジックを純粋なRubyで書けるので、再利用しやすいDRYなコードにできます。
ViewComponentには、「スロット」「コレクション」「条件付きレンダリング」などの機能もあります。
🔗 1: スロット
最近の私はすっかりスロットを多用するようになりました。一度使い所がわかってしまえば、いくらでも使い場所が見つかるはずです(私のRails Designerで販売しているコンポーネントをご覧いただければおわかりいただけるでしょう)。
コンポーネントの例:
- PageHeadingComponent: "Create"や"View"などのページアクションをオプションで指定可能
- ModalComponent: オプションで見出し要素を指定可能
ViewComponentではrenders_oneとrenders_manyという2つのフレーバーを利用できます。以下のサンプルコードをご覧ください。
# blog_component.rb
class BlogComponent < ViewComponent::Base
renders_one :header
renders_many :posts
end
<%# blog_component.html.erb %>
<h1><%= header %></h1>
<% posts.each do |post| %>
<%= post %>
<% end %>
<%= render BlogComponent.new do |component| %>
<% component.with_header do %>
<%= link_to "My blog", root_path %>
<% end %>
<% BlogPost.all.each do |blog_post| %>
<% component.with_post do %>
<%= link_to blog_post.name, blog_post.url %>
<% end %>
<% end %>
<% end %>
これは本当にシンプルな例なので、詳しくはViewComponentのスロットのドキュメントを参照してください。
🔗 2: コレクション
Railsのパーシャルでコレクションをレンダリングできるのと同様に、ViewComponentでもコレクションをサポートしています。以下のサンプルをご覧ください。
<%= render(ProductComponent.with_collection(@products)) %>
class ProductComponent < ViewComponent::Base
def initialize(product:)
@product = product
end
end
私はコレクションを使いすぎないようにする傾向があります。
上のProductComponentクラスに、以下のテンプレートを適用した場合を考えてみましょう。
<li>
<%= @product.name %>
</li>
これではHTMLとして有効にならないうえに、コンポーネントを<ul>で囲むことを忘れないように気をつけなければならなくなります。重要なCSSクラスが脱落する可能性もあります。この書き方はよくありません。
私は、コンポーネントのRubyコードの内部でループを手動で回す方法が、コードを整理するうえで好ましいと思います。
🔗 3: 条件付きレンダリング
条件付きレンダリングは多用しています。
以下のようにERBの中でパーシャルを条件文で囲むのではなく、
<% unless Current.user.subscribed? %>
<%= render partial: "subscribe_form", locals: { user: Current.user} %>
<% end %>
以下のようにコンポーネントをインスタンス化します。
<%= render SubscribeFormComponent.new(user: Current.user) %>
コンポーネントのRubyクラスには以下のようにrender?メソッドも追加します。
class SubscribeFormComponent < ViewComponent::Base
# ...
def render?
!Current.user.subscribed?
end
#...
end
そしてこのrender?の条件に基づいて、コンポーネントをレンダリングするかどうかを決めるようにします。こうすることで、ビューのERBにロジックを書かずに済むので、ビューがかなりすっきりしますよね?
以上は、私がパーシャルの代わりにViewComponentを使うようになったことで得られたメリットの一部です。
当然ながら、ViewComponentを使うならパーシャルは使わないといった2つに1つみたいな話ではまったくありません。私のアプリでは今もパーシャルが使われていますが、パーシャルはシンプルにすべきであり、ロジックは不要なはずです。そしてそうでない場合にこそ、ViewComponentのコンポーネントに移行するようにしています。
関連記事
-
訳注:
config.action_controller.include_all_helpersをfalseに設定すると、ヘルパーの呼び出しは対応するコントローラのスコープ内に限定されます。 ↩ - 訳注: ViewComponent 4.1.1ドキュメントでは最大で2.5倍と改められています。 ↩
概要
元サイトの許諾を得て翻訳・公開いたします。
日本語タイトルは内容に即したものにしました。