Webuilder240

今後Railsで話題になるかもしれないViewComponentを試した

2020-11-08 16:30:38 +0900

Ruby Rails ViewComponent
Rails6.1から「サードパーティー製のコンポーネントフレームワークのサポート」が行われるようです。



そのコンポーネントフレームワークの中でもGitHubが作った
ViewComponent(view_component) というものが存在するので、
今回はこのViewComponentについての解説を簡単にしていきたいと思います。(類似ライブラリについては後述します)


少し補足しておくと、「ActionView::ViewComponent」がRails6.1に取り込まれるというニュアンスで、試してみた記事などがありますが、2020年11月08日現在の状況としては少し異なるように思われるので、Mergeされただけで、正式にリリースされていない情報の場合は、過去の情報はあまり信頼しないで、自分で最新の動向をなるべく仕入れるようにしていきましょう。この記事を更新した後も変わるかもしれませんし。

また、下記PRにもあるようにActionView::ViewComponentはViewComponentにRenameされた模様です。



ViewComponentの概念

このViewComponentの概念、考え方自体は新しいものではなく、広く知られるところでは、Webフロントエンドフレームワークの「Vue」や「React」ではこの概念(単にコンポーネントと呼ばれています)が使われていてより知っている人も増えたと思います。そのコンポーネントの概念をReactからインスパイアを受けて、Railsの世界に持ってきたという感じになります。



GitHubが作ったモチベーション

またこのライブラリを作ったのはGitHubなのですが、GitHubではRailsのViewテンプレートをふんだんに使われており、Viewレイヤーでのテストに問題を抱えていたようです。

これまではシステムテストつまりはブラウザを使ってのE2Eテストないし、コントローラーテストを行う必要があるのですがとても時間がかかります。
ViewComponentではその部分を解決するのとViewデータの流れをわかりやすくするのがメインモチベーションのようです。

簡単な使い方

簡単に使い方というか概要をざっくり書いておきます。
利用している環境は下記の通り。
---
  • Rails 6.0.x 
  • Ruby 2.7.1
Railsの5.0以降、Ruby2.4以降であれば対応していると公式ドキュメントへの記載はありました。
Rails6.1以降、ViewComponent内部で実装されているモンキーパッチがなくなるという感じですかね。

Gemのインストール
下記をRailsアプリケーションのGemfileに追加して、 bundle install しましょう。
gem "view_component", require: "view_component/engine"

RSpecへの対応(Option

Rspecでテストを書きたい人は下記内容をspec/rails_helper.rbに記載しましょう。
require "view_component/test_helpers"

RSpec.configure do |config|
  config.include ViewComponent::TestHelpers, type: :component
end
これでジェネレータを使ってViewComponentを作るときにSpecファイルも作成されます。


コンポーネントを実際に作る
下記のコマンドでサクッと作ってしまいましょう。
今回はブログの一覧ページにあるリストをコンポーネントにしようと思います。
bundle exec rails g component Blog blog
   create  app/components/blog_component.rb
      invoke  rspec
   create  spec/components/blog_component_spec.rb
      invoke  erb
   create    app/components/blog_component.html.erb

ざっくりコンポーネントを実装する

1. 先にBlogComponentの中身を実装しておきます。
// app/components/blog_component.rb
class BlogComponent < ViewComponent::Base
  def initialize(blog:)
    @blog = blog
  end

  def link
    link_to blog.title, blog_path(uuid: blog.uuid)
  end

  def publish_date
    if blog.first_published_at.present?
      blog.first_published_at.strftime("%Y/%m/%d")
    else
      "未公開記事"
    end
  end

  private

  attr_reader :blog
end

テンプレートはこんな感じ。
// app/components/blog_component.html.erb
<li>
  <div>
    <h2> <%= link %> </h2>
    <span>
      <%= publish_date %>
    </span>
    <span>
      <% blog.tags.each do |tag| %>
        <%= link_to tag.name, tag_path(tag.name), class: "blog-tag" %>
      <% end %>
    </span>
  </div>
</li>

上記のようにコンポーネントを作っておくことで、
テンプレート内部の条件分岐が少なくなり、これまでDecorator層やHelperに書いていた実装も、コンポーネントに書いておけそうで、見通しが良くなるかと思います。(今回のサンプルコードでは少ないのであまり恩恵はないような気がしますが。)

2. 下記のように、ブログ一覧画面で、BlogComponentの呼び出しを下記のように行います。
<div class="content">
  <ul class="blog_list">
    <% @blogs.each do |blog| %>
      <%= render(BlogComponent.new(blog: blog)) %>
    <% end %>
  </ul>
</div>
はい、これでViewComponentを使っての出力はできました。簡単ですね。

Rspecでテストを書いてみる

筆者はRspec派なので、Rspecで簡単なテストを書くとどういう感じになるのかを書いておきます。
require "rails_helper"

RSpec.describe BlogComponent, type: :component do
  it "Blogのタイトルが入っていること" do
    blog = Blog.new(title: "test", first_published_at: Time.now, uuid: SecureRandom.uuid)
    expect(render_inline(described_class.new(blog: blog)).css("h2 a").to_html).to include(blog.title)
  end
  it 'ブログの公開日が設定されている場合は「%Y/%m/%d」のフォーマットで表示される' do
    blog = Blog.new(title: "test", first_published_at: Time.now, uuid: SecureRandom.uuid)
    expect(render_inline(described_class.new(blog: blog))
      .css("span.publish_date").to_html).to include(blog.first_published_at.strftime("%Y/%m/%d"))
  end
  it "ブログの公開日が設定されていない場合、「未公開記事」と表示される" do
    blog = Blog.new(title: "test", first_published_at: nil, uuid: SecureRandom.uuid)
    expect(render_inline(described_class.new(blog: blog)).css("span.publish_date").to_html).to include("未公開記事")
  end
end

render_inlineというヘルパーメソッドがComponentSpecに対して生えており、
これはコンポーネントをRenderしてNokogiri::HTML::DocumentFragmentを返すようなメソッドです。
なので.cssメソッドでCSSセレクターでテストを行える...という具合になっているようです。
ひとまずこのようにテストできれば、コントローラーSpecやE2Eのテストよりも高速にテストできるかと思います。

より詳しいことについて
あんまり長くなりすぎても伝えたいことが伝わらない気がするので、今回は概要の紹介にとどめておきます。詳しいことは下記の公式ドキュメントを見るといいと思います。ところでこのドメインはすごい勘違いを生みそうな予感です...




正直、紹介した機能でも必要十分に機能がそろってて、あまり高機能にするとかえって複雑さを増すと思うと個人的には思うんですが、Vueでもある「slot」(実験的ですが)もあるみたいなので、皆様も是非是非使ってみてください。

その他質問や思うところなどなど。


類似ライブラリ
詳しい方はご存じだと思いますが、類似ライブラリで特に有名な trailblazer/cellsが存在しています。



あまり違いを分かっていないのですが、基本的には同じ思想でやろうとしていることは同じで、正直好みで選んでいいと思います。(個人的にはViewComponentのほうがカジュアルに使えそうな感じはする。)
下記は、trailblazer/cellsについて解説している記事です。


またこのほかに

dry-rb/dry-viewや、


Komponent というものも存在するようです。

どういう場合にコンポーネントフレームワークを採用するか?
  • RailsのViewテンプレートやRailsのHelperがカオスで、そのあたりにテストを書かないとやっていられない!!!という場合に採用するといいでしょう。
    • RailsのHelperやDecorator層もよほどViewComponentレイヤーが複雑にならない限りはViewComponet内にメソッドとして書いてしまっていいので、不要になるかもしれないと僕は考えています。
  • 現状のPartialTemplateからComponentに置き換えていくだけでもテストはしやすくなると思うので、RailsのViewをふんだんに使っているプロジェクトは、置き換えも段階にできて簡単なはずなので、「とりあえず置き換えてみる」ということをしてみてもいいのかなぁと思いました。
    • Rails5.0以降、Ruby2.4以降なら導入できるはずなので、結構簡単に導入できると思います。
  • 現状VueやReactを使っている場合は、フロントエンドのレイヤーでComponentへのテストは書けるし、置き換えるメリットは全くないので無視しましょう。

どの粒度でコンポーネントに分ければいいですか?
このあたりはフロントエンド界隈で散々議論されている話題でしょうし、
会社や作っているプロダクトによって変わるというのが答えになると思います。
フロントエンドと異なり、イベントやデータの伝搬(親 -> 子はあるが、子 -> 親はないはず)が大変複雑になることはまぁ起こることはないはずなので、フロントエンドのコンポーネント設計に比べると簡単な気がします。答えはないですが、ReactやVueの記事にこういった話題の投稿があるはずなので、参考になると思います。

関連しそうなブログ