Rails のビューを書いていて、こんな状態に心当たりはありませんか?
- 共通化のつもりで作った
_card.html.erbに、画面ごとのifがどんどん増えていく render "card", user: user, compact: true, show_badge: false, ...とlocalsが伸びて、何を渡せばいいのか分からない- そのパーシャルだけを単体でテストしたいのに、コントローラを経由しないと描画できない
いわゆる「パーシャル地獄」です。共通化のために作ったはずのファイルが、いつの間にか一番読みにくい場所になっている。これを解きほぐすための選択肢が ViewComponent です。
この記事では、パーシャルと ViewComponent の使い分けの結論を先に示したうえで、基本の書き方・パーシャルからの移行手順・スロット・テストまでをコード例つきで解説します。「入れるべきか迷っている」段階の判断材料になれば嬉しいです。
まず結論:どこまでパーシャル、どこから ViewComponent か
先に線引きから示します。ロジックが要らない単純な共通化はパーシャルのままで十分です。わざわざコンポーネント化する必要はありません。
一方で、条件分岐や引数が増えてきた・単体でテストしたい、となったら ViewComponent に移す。この「ロジックとテストが要る単位かどうか」が判断の軸になります。
| こういうビューは… | おすすめ |
|---|---|
| ヘッダー・フッターなど、渡す値が少なくロジックのない共通部品 | パーシャルのまま |
表示の if 分岐が増えてきた/locals が3つ4つと伸びてきた | ViewComponent へ |
| 「バッジの色は状態で変える」など表示ロジックを持つ部品 | ViewComponent へ |
| その部品単体でテストを書いて安心して使い回したい | ViewComponent へ |
ポイントは、最初から全部コンポーネント化しようとしないことです。パーシャルで書いてみて、辛くなったところだけ移す。この順番が現実的です。理由は最後の「ハマりポイント」でも触れます。

ViewComponent とは?パーシャルと何が違うのか
ViewComponent は、GitHub 社が開発している「ビューをオブジェクトとして書く」ための gem です。2026年時点でバージョンは 4系で、機能的に安定した LTS(長期サポート)フェーズに入っています。
ざっくり言うと、1つのコンポーネント=「Ruby のクラス(.rb)」+「テンプレート(.html.erb)」のセットです。パーシャルが「テンプレートだけ」なのに対して、表示に必要なロジックを相棒の Ruby クラス側に書けるのが一番の違いです。
この構造の違いが、次の3つのメリットに直結します。
1. 表示ロジックをクラス側に閉じ込められる
「未読なら赤、既読ならグレー」のような表示の判断を、テンプレートの if ではなく Ruby のメソッドとして書けます。テンプレートは「何を出すか」に集中でき、erb の中がスッキリします。
2. コンポーネント単位でテストが書ける
ViewComponent は Ruby オブジェクトなので、コントローラを経由せず、その部品だけを直接描画してテストできます。「この引数を渡したらこのHTMLが出る」を単体テストで固定できるのは、パーシャルには無かった安心感です。
3. 描画が(少し)速い
ViewComponent のテンプレートはアプリ起動時に一度だけコンパイルされます。リクエストごとにコンパイルされるパーシャルと違い、同じ部品を大量にループで描くようなページでは体感で速くなります。
ただし正直に言うと、数個のパーシャルを描く程度のページでは差はほぼ分かりません。「速さ」は主目的ではなく、あくまで「テストしやすさ・見通しの良さのおまけ」くらいに捉えるのが実態に合っています。
基本の書き方:3ファイルで完結する
では実際に作ってみましょう。まずは Gemfile に追加して bundle install します。
# Gemfile
gem "view_component"あとはジェネレータでコンポーネントを作ります。ここでは「タイトルを受け取って見出しを表示するボタン」を例にします。
bin/rails generate component Button titleこれで次の3ファイルが生成されます。コンポーネント本体・テンプレート・テストがワンセットで出てくるのがポイントです。
app/components/button_component.rb # ← コンポーネント本体(Rubyクラス)
app/components/button_component.html.erb # ← テンプレート
test/components/button_component_test.rb # ← テスト
クラス名は必ず XxxComponent と 末尾が Component になります。この命名は規約なので変えられません。生成された本体はこんな形です。
# app/components/button_component.rb
class ButtonComponent < ViewComponent::Base
def initialize(title:)
@title = title
end
endinitialize で受け取った引数が、そのままテンプレートから使えます。テンプレート側はこう書きます。
<!-- app/components/button_component.html.erb -->
<button class="btn"><%= @title %></button>呼び出すときは、ビューやコントローラから render にインスタンスを渡すだけです。
<%= render(ButtonComponent.new(title: "保存する")) %>render "button", title: ... という文字列指定だったのが、ButtonComponent.new(title: ...) という引数が明示されたオブジェクトに変わりました。「何を渡す必要があるのか」が initialize を見れば一目で分かる。これがパーシャルの locals 地獄から抜け出す第一歩です。
パーシャルからの移行:before / after
言葉だけだとピンと来ないので、実際に「辛くなったパーシャル」を1つ ViewComponent に移してみます。ユーザーのステータスバッジを表示する部品を例にします。パーシャルでのビュー実装そのものを整理したい方は、rails ブログ機能の実装方法7選でも一覧・詳細ページのビューを扱っているので参考になります。
Before:条件分岐が漏れ出したパーシャル
<!-- app/views/users/_status_badge.html.erb -->
<% if user.active? %>
<span class="badge badge-green">有効</span>
<% elsif user.invited? %>
<span class="badge badge-yellow">招待中</span>
<% else %>
<span class="badge badge-gray">停止中</span>
<% end %>「色」と「ラベル」を決める分岐が、テンプレートの中にべったり書かれています。表示のパターンが増えるほど、この if は伸びていきます。
After:判断をクラスに移す
色とラベルを決めるロジックを Ruby クラス側のメソッドに追い出します。
# app/components/status_badge_component.rb
class StatusBadgeComponent < ViewComponent::Base
def initialize(user:)
@user = user
end
def label
{ active: "有効", invited: "招待中", suspended: "停止中" }[status]
end
def color_class
{ active: "badge-green", invited: "badge-yellow", suspended: "badge-gray" }[status]
end
private
def status
return :active if @user.active?
return :invited if @user.invited?
:suspended
end
endテンプレートは、分岐が消えて「メソッドを呼ぶだけ」になりました。
<!-- app/components/status_badge_component.html.erb -->
<span class="badge <%= color_class %>"><%= label %></span>呼び出し側はこうです。
<%= render(StatusBadgeComponent.new(user: user)) %>何が良くなったかというと、「色とラベルの対応」という仕様が1つのクラスに集約された点です。ステータスが増えたときに直す場所が明確になり、しかも次に説明するテストでその対応表を丸ごと固定できるようになります。
コンポーネントは単体でテストできる
ViewComponent の一番おいしいところが、この単体テストです。コントローラもルーティングも要らず、コンポーネントを直接描画して中身を検証できます。
# test/components/status_badge_component_test.rb
require "test_helper"
class StatusBadgeComponentTest < ViewComponent::TestCase
test "招待中のユーザーは黄色いバッジになる" do
user = User.new(state: :invited)
render_inline(StatusBadgeComponent.new(user: user))
assert_text "招待中"
assert_selector "span.badge-yellow"
end
endrender_inline でコンポーネントを描画し、assert_selector や assert_text で出力を検証します。「招待中なら黄色」という表示の仕様そのものをテストで守れるので、あとからステータスを追加してもデグレに気づけます。パーシャルでこれをやろうとすると、コントローラ経由のシステムテストが必要でした。その手間が消えるのは大きいです。
スロット:中身を差し込める箱を作る
「カードの枠は共通だけど、ヘッダーと本文の中身は呼び出し側で自由に入れたい」。こういうときに使うのがスロットです。パーシャルの content_for / yield のもっと整理された版だと思ってください。
スロットは、モデルの has_one / has_many と同じ感覚で renders_one / renders_many を使って定義します。
renders_one:1回だけ差し込めるスロット(例:カードのヘッダー)renders_many:複数回差し込めるスロット(例:リストの各行)
# app/components/card_component.rb
class CardComponent < ViewComponent::Base
renders_one :header
renders_many :rows
endテンプレートでは、差し込まれた中身を header / rows で呼び出します。
<!-- app/components/card_component.html.erb -->
<div class="card">
<div class="card-header"><%= header %></div>
<% rows.each do |row| %>
<div class="card-row"><%= row %></div>
<% end %>
</div>呼び出し側は、ブロックの中で with_header / with_row を使って中身を流し込みます。
<%= render(CardComponent.new) do |card| %>
<% card.with_header do %>
<h2>メンバー一覧</h2>
<% end %>
<% @users.each do |user| %>
<% card.with_row do %>
<%= user.name %>
<% end %>
<% end %>
<% end %>枠(レイアウト)はコンポーネント側、中身は呼び出し側、と役割がきれいに分かれます。locals でHTMLの塊を文字列で渡すような無理をしなくて済むのがスロットの利点です。
ハマりポイント逆引き
導入時につまずきやすい点を、症状ベースで並べておきます。
| 症状 | 原因と対処 |
|---|---|
| コンポーネントが読み込まれない・定数エラー | クラス名が XxxComponent になっていない。末尾 Component は必須。ファイル名も xxx_component.rb に揃える |
| テンプレートが見つからない | .rb と .html.erb のファイル名が対応していない(button_component.rb なら button_component.html.erb) |
link_to やルーティングヘルパーが undefined method になる | アプリ独自のヘルパーやパス系ヘルパーは直接呼べないことがある。helpers. を頭に付ける(例:helpers.user_path(user)) |
| 作りすぎて逆に見通しが悪くなった | 小さすぎる単位までコンポーネント化しない。ロジックのない1行の共通化はパーシャルで十分 |
特に最後の「作りすぎ」は本当によくあります。ViewComponent は便利ですが、ロジックもテストも要らない部品まで全部コンポーネントにすると、ファイルが増えるだけで見通しは悪化します。冒頭の使い分け表に戻って、「これはロジックを持つか?テストしたいか?」で立ち止まるのがコツです。
よく使うメソッド早見表
実際に書くときに手が止まらないよう、ViewComponent で最初に覚える分だけまとめておきます。まずはこれだけで一通り動きます。
| 書くもの | 役割 |
|---|---|
class XxxComponent < ViewComponent::Base | コンポーネントの基底クラスを継承する(末尾 Component は必須) |
def initialize(...) | 受け取る引数を定義する。ここがそのまま「渡すべき値」の一覧になる |
render(XxxComponent.new(...)) | ビューやコントローラから呼び出す |
renders_one / renders_many | 中身を差し込めるスロットを定義する |
with_xxx do ... end | 呼び出し側からスロットに中身を流し込む |
render_inline(...) | テストでコンポーネントを直接描画する |
helpers.xxx | アプリ独自のヘルパーやパスヘルパーを呼ぶときに頭に付ける |
使い分けチェックリスト
最後に、導入判断のチェックリストをまとめておきます。
- そのビューは表示ロジック(条件分岐・整形)を持っているか → 持つなら ViewComponent
localsの引数が3つ以上に増えていないか → 増えているなら ViewComponent- その部品を単体でテストしたいか → したいなら ViewComponent
- 同じ部品を大量にループ描画していて速度が気になるか → 気になるなら ViewComponent
- 上のどれにも当てはまらない単純な共通化か → パーシャルのままでOK
まとめ
ViewComponent は、パーシャルが「ロジックとテストに耐えられなくなった」ところを引き取るための道具です。ポイントを振り返ります。
- 使い分けの軸は「ロジックとテストが要るか」。単純な共通化はパーシャルのままでいい
- 1コンポーネント=Rubyクラス+テンプレート。表示ロジックをクラスに閉じ込められる
render_inlineで単体テストできるのが最大の利点。表示の仕様をテストで守れる- 複雑なレイアウトはスロット(
renders_one/renders_many)で中身を差し込む - 作りすぎ注意。辛くなったパーシャルだけを移す、が現実的
まずは、いま一番 if が膨らんでいるパーシャルを1つだけ ViewComponent に移してみてください。テストが書けた瞬間に、「これは便利だ」と実感できるはずです。
Rails のビュー実装をもっと広く見直したい方は、rails ブログ機能の実装方法7選もあわせてどうぞ。環境構築からやり直したい場合はmacOSにRuby on Railsをインストールを、エラーで詰まったときの調べ方はエンジニアの仕事は「知っていること」より「調べられること」を参考にしてください。
スポンサーリンク
