【Rails】ViewComponentを使って再利用可能なコンポーネントを作成する

今回は再利用可能なUIコンポーネントを作成するためのフレームワークであるViewComponentを使ってみます。
公式ドキュメント
https://viewcomponent.org/
GitHub
https://github.com/viewcomponent/view_component
ViewComponentとは
公式によるとReactから影響を受けたpresenter patternという手法を進化させたようなものだそうです。
ViewComponents are Ruby objects used to build markup. Think of them as an evolution of the presenter pattern, inspired by React.
インストール
今回は以下のバージョンで動作確認を行なっています。
$ rails -v
Rails 8.0.1
$ ruby -v
ruby 3.3.4
rails new
実行時のオプションとしては以下を指定しています。
JavaScript
のビルドにESBuild
を使用css
フレームワークのTailwind CSS
を使用
gemfile
にview_component
を追加します。
gem "view_component"
インストールします。
$ bundle install
generator実行
インストール後、コンポーネント名とコンポーネントで使用する引数を指定してファイルを生成するgenerate
コマンドが使えるようになります。
$ rails generate component コンポーネント名 引数1 引数2
見出しコンポーネントを作成してみます。
$ rails generate component Heading size text
components
ディレクトリ、その配下に2つのファイルが作成されました。
# frozen_string_literal: true
class HeadingComponent < ViewComponent::Base
def initialize(size:, text:)
@size = size
@text = text
end
end
<div>Add Heading template here</div>
コンポーネント名_component.rb
ファイルで引数を受け取って初期化や、ロジックを記述、コンポーネント名_component.html.erb
ファイルでレンダリングを行います。
基本的な使い方
実際に使ってみます。
表示確認
まずは表示確認用のページを準備します。
コントローラー、viewファイル作成
$ rails g controller home index
ルーティング追加
Rails.application.routes.draw do
# 省略
root "home#index" # 追加
end
ルートページに以下のように記述します。
今回size
, text
という引数を指定したので、一旦適当に値を入れておきます。
<%= render(HeadingComponent.new(size: "h1", text: "見出し1")) %>
ルートページにアクセスするとheading_component.html.erb
の内容が表示されます。

インスタンス変数を表示
heading_component.html.erb
ではheading_component.rb
で初期化時に定義されたインスタンス変数を利用できます。
heading_component.html.erb
を次のように修正します。
<div><%= @text %></div>
ルートページにアクセスすると先ほどtext
引数に指定した文字が表示されています。
ロジックを追加
components
ディレクトリ配下にもTailwindCSS
のスタイルを適応できるようにtailwind.config.js
にパスを追加します。
module.exports = {
content: [
'./app/views/**/*.html.erb',
'./app/components/**', // 追加
'./app/helpers/**/*.rb',
'./app/assets/stylesheets/**/*.css',
'./app/javascript/**/*.js'
]
}
heading_component.rb
にsize
引数の値に応じたTailwindCSS
のクラスを返すメソッドを追加します。
# frozen_string_literal: true
class HeadingComponent < ViewComponent::Base
def initialize(size:, text:)
@size = size
@text = text
end
# 追加
def font_size
case @size
when "h1"
"text-2xl"
when "h2"
"text-xl"
when "h3"
"text-lg"
else
"text-md"
end
end
end
heading_component.html.erb
でクラスを適応するように修正します。
<div class="<%= class_names("font-bold", font_size) %>"><%= @text %></div>
ルートページに4パターン表示してみます。
<%= render(HeadingComponent.new(size: "h1", text: "見出し1")) %>
<%= render(HeadingComponent.new(size: "h2", text: "見出し2")) %>
<%= render(HeadingComponent.new(size: "h3", text: "見出し3")) %>
<%= render(HeadingComponent.new(size: "h4", text: "見出し4")) %>
ルートページにアクセスするとsize
引数によってスタイルを変えることができているのが確認できます。

現状全てdiv
タグでレンダリングされているのでsize
引数で渡した値のタグを生成するように変更します。
<%= content_tag(@size.to_sym, @text, class: class_names("font-bold", font_size)) %>
再度ルートページにアクセスするとsize引数に渡した値でタグが生成されていることが確認できます。

その他機能
コレクション
partial
を繰り返し表示する際によく使うcollection
の指定をViewComponent
でも同様に行うことができます。
まずはpartial
で実装してみます。
何かしらデータがあった方が良いので適当にCategory
モデルを作成します。
モデル、テーブル作成
$ rails g model Category name
$ rails db:migrate
コントローラー、viewファイル作成
$ rails g controller categories index
ルーティング追加
Rails.application.routes.draw do
# 省略
resources :categories, only: :index # 追加
root "home#index"
end
コントローラのindex
アクションで全てのカテゴリを取得します。
class CategoriesController < ApplicationController
def index
@categories = Category.order(created_at: :desc)
end
end
app/views/categories
配下に_category.html.erb
を以下の内容で作成します。
<%= category.name %><br/>
作成したpartial
をcollection
を使って表示します。
せっかくなので先ほど作成した見出しコンポーネントも使ってあげます。
<%= render(HeadingComponent.new(size: "h1", text: "カテゴリ一覧")) %>
<br/>
<%= render(HeadingComponent.new(size: "h2", text: "パーシャルを使った表示")) %>
<%= render partial: "category", collection: @categories %>
ちなみに今回の場合は<%= render @categories %>
のように省略しても書けます。
rails console
でデータをいくつか入れます
$ rails c
> Category.create(name: "経済")
> Category.create(name: "IT")
> Category.create(name: "金融")
/categories
にアクセスします。
カテゴリの一覧が表示できていることが確認できます。

同じことをViewComponent
を使って行います。
カテゴリコンポーネントファイルを生成します。
$ rails generate component Category category
viewファイル側を以下のように変更します。
<%= @category.name %><br/>
カテゴリ一覧ページでViewComponent
を使用したパターンを追加します。
<%= render(HeadingComponent.new(size: "h1", text: "カテゴリ一覧")) %>
<br/>
<%= render(HeadingComponent.new(size: "h2", text: "パーシャルを使った表示")) %>
<%= render @categories %>
<br/>
<%# 追加 %>
<%= render(HeadingComponent.new(size: "h2", text: "ViewComponentを使った表示")) %>
<%= render(CategoryComponent.with_collection(@categories)) %>
/categories
にアクセスするとpartial
を使った時と同じように表示ができていることが確認できます。

条件付きレンダリング
もし、ユーザーがログインしているときだけそのユーザーのアイコンを表示したいという場合、以下のようにviewファイル内にif文を追加することが多いかと思います。
<% if user_signed_in? %>
<%= render(UserIconComponent.new(user: current_user)) %>
<% end %>
ViewComponent
ではクラスファイルの方にrender?
というメソッドを定義して表示条件を指定することができます。
# frozen_string_literal: true
class HeadingComponent < ViewComponent::Base
include Devise::Controllers::Helpers
def initialize(user:)
@user = user
end
def render?
@user.present?
end
end
この定義により、viewファイル側でif文を記述する必要がなくなります。
<%= render(UserIconComponent.new(user: current_user)) %>
テスト
ViewComponentではコンポーネント単位でテストを実行できます。
今回はRSpecを使って試してみます。
gemfile
にrspec-rails
, capybara
を追加します。
# 省略
group :test do
gem "capybara"
end
group :development, :test do
# 省略
gem "rspec-rails"
end
# 省略
インストールしてrspec関連のファイルを生成します。
$ bundle install
$ rails generate rspec:install
ViewComponent
のテストができるように設定を追加します。
require "view_component/test_helpers"
# 省略
RSpec.configure do |config|
# 省略
config.include ViewComponent::TestHelpers, type: :component
end
specディレクトリにcomponentsフォルダを作成して、以下の内容でカテゴリコンポーネントのテスト用ファイルを作成します。
require "rails_helper"
RSpec.describe CategoryComponent, type: :component do
let(:category) { Category.new(name: "Test Category") }
it "カテゴリ名が表示される" do
render_inline(CategoryComponent.new(category: category))
expect(page).to have_content("Test Category")
end
end
bundle exec rspec
を実行してテストが通れば完了です。
$ bundle exec rspec
.
Finished in 0.09086 seconds (files took 1.23 seconds to load)
1 example, 0 failures
このようにrender_inline
メソッドを使ってコンポーネント単位でテストを行うことができます。
View Component Contrib
ViewComponent
はデフォルトではapp/components
フォルダ配下に2つのファイルが追加されていきます。
このままではcomponents
内がファイルだらけになって管理がしづらそうな印象です。
そこでview_component-contribというgemを使うと、生成するフォルダの場所を指定できたり、generatorのカスタマイズなどが可能になります。
インストール
※view_componentは未インストール, RSpecをインストール済みの状態で進めます。
READMEのコマンドを実行します。
rails app:template LOCATION="https://railsbytes.com/script/zJosO5"
どこにコンポーネントファイルを保管したいか聞かれるのでapp/views/components
とします。
Where do you want to store your view components? (default: app/frontend/components) app/views/components
Would you like to use dry-initializer in your component classes? (y/n) y
stimulus
を使うか聞かれるのでy
としておきます。
今回は触れませんが、設定については以下のディスカッションを参考にというメッセージが出ます。
https://github.com/palkan/view_component-contrib/discussions/14
Do you use Stimulus? (y/n) y
TaliwindCSS
を使うか聞かれるのでy
とします。
Do you use TailwindCSS? (y/n) y
カスタムgeneratorを使うか聞かれるのでy
とします。
Would you like to create a custom generator for your setup? (y/n) y
テンプレートはERB
を使うので1とします。
Which template processor do you use? (1) ERB, (2) Haml, (3) Slim, (0) Other 1
実行が完了したら設定ファイルが生成されます。
lib/generators
配下にはgeneratorのテンプレートファイルが作成されています。
今回はRSpecをインストールしていたので、テストのテンプレートとしては
- component_spec.rb.tt
- component_system_spec.rb.tt
の2つが作成されていました。
app/views
配下には指定した通りにcomponentsディレクトリが作成され、基底クラスである、
- application_view_component.rb
- application_view_component_preview.rb
が作成されています。
その他にもconfig/application.rb
やspec/rails_helper.rb
などでも設定が追加されています。
見出しコンポーネントを作成
先ほどと同じように見出しコンポーネントを作成してみます。
generatorコマンドを実行します。今回はcomponentの箇所がview_componentとなっているので注意が必要です。
$ rails generate view_component Heading size text
テンプレートの箇所で定義されているファイルがそれぞれ生成されます。
コンポーネントのクラスファイルでは先ほど選択したdry-initializer
を使っていることでinitialize
メソッドの箇所が簡潔に書けるようになっています。
# frozen_string_literal: true
class Heading::Component < ApplicationViewComponent
with_collection_parameter :heading
option :size
option :text
end
コンポーネントを修正して表示してみます。
# frozen_string_literal: true
class Heading::Component < ApplicationViewComponent
option :size
option :text
def font_size
case @size
when "h1"
"text-2xl"
when "h2"
"text-xl"
when "h3"
"text-lg"
else
"text-md"
end
end
end
※with_collection_parameter :heading
の箇所は必要ないので削除しています。
<%= content_tag(@size.to_sym, @text, class: class_names("font-bold", font_size)) %>
<%= render(Heading::Component.new(text: "見出し1", size: "h1")) %>
<%= render(Heading::Component.new(text: "見出し2", size: "h2")) %>
<%= render(Heading::Component.new(text: "見出し3", size: "h3")) %>
<%= render(Heading::Component.new(text: "見出し4", size: "h4")) %>
ルートページにアクセスすると先ほどと同じように表示できていることが確認できます。

view_component-contrib
を適切に使うことでViewComponent
がより扱いやすくなりそうです。
他にもStyle Variantsという機能を使ってTailwindCSSなどを活用したカスタマイズ性の高いコンポーネントも作成できそうなので参考にしてみてください。
参考