ViewComponentを紹介したい
はじめまして、プログラミングスクールRUNTEQを卒業したばかりのzakiと申します!!
今回はたまたま見つけたRailsのGem ViewComponentがあまりにも便利だったので紹介させてください。
もしよろしければ使ってみた感想のや使い方の記事を作ってください、絶対に見に行きます。
経緯
画像の完成予想図のような画面にしようとしていました。
主に
- ヘッダー
- ベントの情報をまとめた部分
- TODOの情報をまとめた部分
- 戻るボタン
の4つに分けられます。
-
画像① 完成予想図
こんな感じの画面を作りたい。
これを初学者が作ろうとすると画像②のようになりました。
-
画像② 作成画面
作成途中画面、ハードコーディングやもろもろ不細工なのはお許しください。
この時点でshow.html.erbの記述は290行になっている。
コード長すぎ!!
個人的には1つのファイルに記載されているコードは50行程度に収めたいのに約6倍のコードになってやがる!!あとでリファクタリングしたい時に該当箇所を探すのが大変やんけ!!
といいうことでshowファイルのコードをスリム化させようと試行錯誤が始まりました。
目的
- showファイルの全体的なスリム化を行いたい
- パーシャルを増やしすぎると若干のオーバーヘッドが起きるらしいので代案を使いたい
- Railsの基本原則、単一責任の原則に則りたい。
- 各UIをコンポーネント化してリファクタリングで追いかける際に楽をしたい。
前提
筆者環境
プログラミングスクールで勉強中の初学者、「scaffoldってなんですか?」レベル。
初のwebアプリ制作時にview componentを導入した。
開発環境
本記事で使用している環境は以下の通りです:
項目 | バージョン・詳細 |
---|---|
Ruby | 3.3.6 |
Rails | 7.2.1 |
ViewComponent | 3.21.0 |
テスト環境 | RSpec |
フロントエンド | Bootstrap 5.3.3, Stimulus JS |
データベース | PostgreSQL |
開発環境 | Docker |
エディタ | Visual Studio Code (VSCode) |
📝 記事内のサンプルコードは上記環境で動作確認していますが、ViewComponentの基本機能は多くの環境で同様に動作するはずです。
対象者
- viewファイルのスリム化を行いたい方向け
View componentsとは
ようやく本題です。
View componentsはRailsのAction ViewでReactライクにHTMLコーディングができるGemです。
平たくいうとテンプレートの各パーツをカプセル化して再利用できるようにしようぜ。という発想のもと生まれました。
パーシャルと違いView componentsを導入することによるメリットは下記のとおりとなります。
- データの受け渡しが明確で安全
- ロジックを適切な場所に配置できる
- テストが書きやすい(らしい)
- 再利用性が高い
- プレビュー機能でデザインを確認しやすい
- パーシャルと比べて読み込みが10倍早い(らしい)
特にロジックを適切に配置できるという点が私的にはだいぶ助かりました。
View componentsの導入
Gemfileに以下を追記します。
gem "view_component"
viewcomponentsはrails generate component
コマンドが使用できます。
試しにdocker compose exec web rails g component Example title content
を実行してみます。
app/componentsというディレクトリにexample_component.rb
とexample_component.html.erb
というファイルが二つできました。テストファイルもできましたが今回は触れません。
rails generate component
コマンドで指定した title
とcontent
という引数が app/components/example_component.rb の initialize
に渡されるようになっています。
# frozen_string_literal: true
class ExampleComponent < ViewComponent::Base
def initialize(title:, content:)
@title = title
@content = content
end
end
現在のままだとapp/componentsディレクトリに.rbファイルと.html.erbファイルがセットで生成さるので複数のコンポーネントを生成していくと一気に視認性が悪くなります。
そのためdocker compose exec web rails generate component Sections::Example title content
を実行しサブディレクトリを作成しある程度グループ分けできるようにします。
※eventsとshardディレクトリは気にしないでください。お願いします。
私は今回使いませんでしたがジェネレータコマンドのオプションでプレビューファイルやstimulus用のコントローラーも生成できるみたいです。
bin/rails generate component Example title --preview
create app/components/example_component.rb
invoke test_unit
create test/components/example_component_test.rb
invoke preview
create test/components/previews/example_component_preview.rb
invoke erb
create app/components/example_component.html.erb
bin/rails generate component Example title --stimulus
create app/components/example_component.rb
invoke test_unit
create test/components/example_component_test.rb
invoke stimulus
create app/components/example_component_controller.js
invoke erb
create app/components/example_component.html.erb
実際に使ってみた
以下が最初期に作ったコードの一部です。
# /app/views/events/show.html.erb
<!-- ビューにロジックが混在 -->
<!-- 操作ボタン(メモカードの下部に配置) -->
<% if user_signed_in? && current_user.id == @event.user_id %>
<div class="d-flex justify-content-end">
<%= link_to edit_event_path(@event), class: "btn btn-primary me-2" do %>
<i class="bi bi-pencil me-1"></i>編集
<% end %>
<%= button_to event_path(@event), method: :delete, class: "btn btn-danger",
form: { data: { turbo_confirm: "このイベントを削除してもよろしいですか?" } } do %>
<i class="bi bi-trash me-1"></i>削除
<% end %>
</div>
<% end %>
上記は
①現在のユーザーがログインしているか
②現在のユーザーがイベントを作成したユーザーか
の2つの条件分岐を行っています。2つの条件がtrueであれば編集と削除ボタンが表示されます。
しかしrailsのviewにはロジックは書かないという考えに違反しているのでよくない記述になります。
あとなんかこんなのがいっぱいでごちゃごちゃしてて見にくい
これをview componentsを使うと
# /app/components/events/info/status_badge_component.rb
module Events
module Info
class ActionButtonsComponent < ViewComponent::Base
attr_reader :event, :current_user
def initialize(event:, current_user:)
@event = event
@current_user = current_user
end
def render?
current_user && current_user.id == event.user_id
end
def confirm_message
"このイベントを削除してもよろしいですか?"
end
end
end
end
# app/components/events/info/action_buttons_component.html.erb
<div class="d-flex justify-content-end">
<%= link_to edit_event_path(event), class: "btn btn-primary me-2" do %>
<i class="bi bi-pencil me-1"></i>編集
<% end %>
<%= button_to event_path(event),
method: :delete,
class: "btn btn-danger",
form: { data: { turbo_confirm: confirm_message } } do %>
<i class="bi bi-trash me-1"></i>削除
<% end %>
</div>
ロジックとviewを完全に分けることができます。
あとめっちゃ見やすくなりました!!
実際のshow.html
# app/views/events/show.html.erb
・・・省略・・・
<div>
<%= render(Events::Info::StatusBadgeComponent.new(event: @event)) %>
</div>
・・・省略・・・
show.htmlに書く部分は<%= render(Events::Info::StatusBadgeComponent.new(event: @event)) %>
だけで済みます。
render? メソッドについて
これは特別なメソッドで、ViewComponentでコンポーネントを表示するかどうかを決定するために使われます。
render?
メソッドは ViewComponent::Base
クラスで定義されているメソッドをオーバーライドしています。
render?メソッド定義している条件分岐がtrueの場合のみコンポーネントを表示しfalseの場合は何もせずに処理を終了させます。
従来、ビューをレンダリングするかどうかのロジックは、コンポーネントかhtml.erbファイルのいずれかに記述することができました。
よくあるhtml.erbファイルの場合
<% if user.requires_confirmation? %>
<div class="alert">Please confirm your email address.</div>
<% end %>
またはviewcomponentsを普通に使った場合
<% if current_user.requires_confirmation?%>
<%= render(ConfirmEmailComponent.new(user: current_user))%>
<% end%>
これをrender?メソッドを使うことでシンプルにできます。
viewcomponentsのクラスファイル
class ConfirmEmailComponent < ViewComponent::Base
erb_template <<-ERB
<div class="banner">
Please confirm your email address.
</div>
ERB
def initialize(user:)
@user = user
end
def render?
@user.requires_confirmation?
end
end
html.erbファイルなど
<%= render(ConfirmEmailComponent.new(user: current_user)) %>
まとめ
view componentsを使うことによりパーシャルに比べviewとクラスファイルをより細かく分けられるのでどこに問題があるのかより追いやすくなりました。
また、最終的にshow.html.erbファイルもコンポーネントをレンダリングするだけなのでファイル自体に記述する量も大幅に減らせることができます。全てコンポーネントにするとファイル量が大幅に増えてしまうのでそこだけ注意してください。
view componentの大きな利点でテストも各コンポーネントごとに単体テストできる点や、プレビューできる機能もあるのでそこを掘り下げられたらまた別途記事を作成していきたいと思います!!
ここまで見てくださってありがとうございました!!
参考文献
- 公式ドキュメント
https://viewcomponent.org/ - RailsのViewComponentを触ってみた
https://zenn.dev/tor_inc/articles/43e3ad4a99d29c - ViewComponent を試してみた
https://zenn.dev/cobachie/articles/tried-view-component
Discussion