ViewComponent を試してみた
はじめに
Rails 6.1 から ViewComponent がデフォルトでサポートされるようになったそうなので試してみました。
ViewComponent
ViewComponent は HTML を出力する Ruby オブジェクトです。
使いどころとしては部分テンプレート(partial)と同じと考えればよさそうですが、ViewComponent をつかうことでユニットテストができたり、partial よりパフォーマンスがよかったりというメリットがあります。
前提
$ ruby -v
ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-darwin19]
$ bin/rails -v
Rails 6.1.3
今回も Rails Girls アプリ・チュートリアル でつくるアイデア投稿アプリをベースにして、一覧画面の各アイデアの部分をコンポーネントにしてみようと思います。また、テストには RSpec を使うので、Rails Girls の Test your app with RSpec を参考にインストールしておきます。
実装
view_component gem を追加
gem "view_component", require: "view_component/engine"
component を生成する
rails generate component
コマンドが利用できます。コンポーネントの名前と、コンポーネントに渡す引数のリストを指定して実行します。
$ bin/rails g component Idea idea
Running via Spring preloader in process 68330
create app/components/idea_component.rb
invoke rspec
create spec/components/idea_component_spec.rb
invoke erb
create app/components/idea_component.html.erb
app/components
というディレクトリができて、その下に rb と html.erb のファイルが生成されました。
app/components/
├── idea_component.html.erb
└── idea_component.rb
rails generate component
コマンドで指定した idea
という引数が app/components/idea_component.rb の initialize
に渡されるようになっています。
# frozen_string_literal: true
class IdeaComponent < ViewComponent::Base
def initialize(idea:)
@idea = idea
end
end
一覧画面のアイデアの部分をコンポーネント化する
app/components/idea_component.html.erb に 一覧画面のアイデアを表示する部分を切り出します。アイデアのオブジェクトは、IdeaComponent
の @idea
を利用します。
# app/components/idea_component.html.erb
+ <div class="col-md-4">
+ <%= image_tag @idea.picture_url, width: '100%' if @idea.picture.present? %>
+ <h4><%= link_to @idea.name, @idea %></h4>
+ <%= @idea.description %>
+ </div>
そして、呼び出し側の一覧画面では render
メソッドの引数にIdeaComponent.new(idea: idea)
を渡します。
# app/views/ideas/index.html.erb
...
<section data-controller="ideas">
<% @ideas.in_groups_of(3) do |group| %>
<div class="row">
<% group.compact.each do |idea| %>
- <div class="col-md-4">
- <%= image_tag idea.picture_url, width: '100%' if idea.picture.present? %>
- <h4><%= link_to idea.name, idea %></h4>
- <%= idea.description %>
- </div>
+ <%= render(IdeaComponent.new(idea: idea)) %>
<% end %>
</div>
<% end %>
</section>
...
部品の実装
Decorator や Helper などに書いていたようなコードも Component 内で書くことができるので View がすっきりします。例えば image_tag
を Component に移すとこんな感じになります。
# app/components/idea_component.rb
class IdeaComponent < ViewComponent::Base
...
+ def image
+ return if idea.picture.blank?
+
+ image_tag @idea.picture_url, width: '100%'
+ end
+ private attr_reader :idea
end
(ちなみに private attr_reader :idea
は Ruby 3.0 で導入された書き方になります。)
IdeaComponent で定義したメソッドは erb でそのまま呼び出せます。
<%# app/components/idea_component.html.erb %>
<div class="col-md-4">
- <%= image_tag @idea.picture_url, width: '100%' if @idea.picture.present? %>
+ <%= image %>
<h4><%= link_to @idea.name, @idea %></h4>
<%= @idea.description %>
</div>
RSpec
個人的に ViewComponent の一番いいなと思った点はテストが書きやすいということです。HTML をテストしたいとき今は E2E テストを書くことが多いのですが、実行に時間がかかったり、たまに落ちるテストがあったりであまり快適ではありません。ViewComponent であればコンポーネント単位にテストを書けますし、実行時間も早いので躊躇せずにテストを増やせそうな気がします。
最初に rspec_helper.rb で ViewComponent::TestHelpers を include します。
require "view_component/test_helpers"
RSpec.configure do |config|
config.include ViewComponent::TestHelpers, type: :component
end
IdeaComponent はこんな感じで書けます。
require "rails_helper"
RSpec.describe IdeaComponent, type: :component do
let(:idea) { Idea.new(name: 'Awesome Idea', description: 'Something interesting.', picture: 'awesome.png') }
subject(:component) { render_inline(described_class.new(idea: idea)) }
it "アイデアの名前を表示する" do
expect(component.css("h4").to_html).to include idea.name
end
it "アイデアの説明を表示する" do
expect(component.to_html).to include idea.description
end
end
ディレクトリ構成
デフォルトでは app/components 下にフラットにファイルを置くようになっていますが、ディレクトリを分けてファイルを配置することもできるようです。
rails generate component
コマンドを --sidecar
オブションを付けて実行すると、ideas2_component
ディレクトリの中に html ファイルが配置されます。component-file-inside-sidecar-directory
bin/rails g component Idea2 idea --sidecar
Running via Spring preloader in process 77984
create app/components/idea2_component.rb
invoke rspec
create spec/components/idea2_component_spec.rb
invoke erb
create app/components/idea2_component/idea2_component.html.erb
また、rb ファイルもディレクトリ内に置きたい場合は以下のようにもできます。component-file-inside-sidecar-directory
app/components/
└── idea2
├── component.html.erb
└── component.rb
class Idea2::Component < ViewComponent::Base
...
end
<%= render(Idea2::Component.new(idea: idea)) %>
さいごに
使いやすいし早いしテストもできて安心なので、これからは部分テンプレートではなく ViewComponent を使うようにしたらよさそう!と思いました。Stimulus を併用する場合についてはまだ試せていないので今度やってみたいと思います。
Discussion