🐣

ViewComponentを紹介したい

に公開

はじめまして、プログラミングスクールRUNTEQを卒業したばかりのzakiと申します!!

今回はたまたま見つけたRailsのGem ViewComponentがあまりにも便利だったので紹介させてください。

もしよろしければ使ってみた感想のや使い方の記事を作ってください、絶対に見に行きます。

経緯

画像の完成予想図のような画面にしようとしていました。

主に

  1. ヘッダー
  2. ベントの情報をまとめた部分
  3. TODOの情報をまとめた部分
  4. 戻るボタン

の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.rbexample_component.html.erb というファイルが二つできました。テストファイルもできましたが今回は触れません。

rails generate component コマンドで指定した titlecontent という引数が 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の大きな利点でテストも各コンポーネントごとに単体テストできる点や、プレビューできる機能もあるのでそこを掘り下げられたらまた別途記事を作成していきたいと思います!!

ここまで見てくださってありがとうございました!!

参考文献

Discussion