🌟

ViewComponent を試してみた

2021/03/06に公開

はじめに

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