Abstrakte Illustration: modulare Bausteine aus Code fügen sich zu einer Weboberfläche zusammen

Rails View Components: Cells, ViewComponent und Phlex

Ein Partial namens _card.html.erb liegt im Verzeichnis app/views/shared. Es bekommt fünf lokale Variablen, zwei davon optional. Ob ein Footer erscheint, entscheidet ein defined? mitten im HTML-Code. Niemand traut sich mehr, es anzufassen. Für diese Lage gibt es Rails View Components: Klassen, die ein Stück Oberfläche samt Logik kapseln und sich wie normales Ruby testen lassen. Drei Ansätze haben sich etabliert. Hier findest du Cells, ViewComponent und Phlex mit Code im Vergleich, dazu eine Checkliste für die Entscheidung.

Warum Partials und Helper irgendwann nicht mehr reichen

Rails bringt für Views drei Werkzeuge mit: Layouts für den Seitenrahmen mit yield, Partials für wiederkehrende HTML-Blöcke und Helper für kleine Ausgabelogik. Das reicht, solange eine App überschaubar bleibt. Ein Partial bekommt seine Daten über den Hash locals:, so beschreibt es auch der Rails-Guide zu Layouts und Rendering. Welche Variablen Pflicht sind, steht nirgends. Ein Tippfehler im Namen fällt erst zur Laufzeit auf.

So sieht so ein Fall typischerweise aus. Die View der index-Action einer Produktliste ruft das Partial shared/card für jeden Eintrag auf:

<%# app/views/products/index.html.erb %>
<h1>Listing Products</h1>
<% @products.each do |product| %>
  <%= render "shared/card", title: product.name, content: product.description,
             link: edit_product_path(product) %>
<% end %>
<%# app/views/shared/_card.html.erb %>
<div class="card">
  <h2><%= title %></h2>
  <div class="card-content"><%= content %></div>
  <%= link_to "Bearbeiten", link if defined?(link) %>
  <% if defined?(footer) %>
    <div class="card-footer"><%= footer %></div>
  <% end %>
</div>

Die Datei _card.html.erb wird an drei Stellen verwendet: in der Liste, auf der Show-Seite und im Edit-Formular. Jede Stelle übergibt etwas andere Variablen, und die CSS-Klasse card-footer taucht nur auf, wenn jemand an footer gedacht hat. Was davon wirklich ausgegeben wird, siehst du erst, wenn du alle Aufrufe im Verzeichnis app/views durchsuchst.

Helper haben ein anderes Problem. Jede Methode in app/helpers landet im globalen Namensraum aller Views. Bei 40 Helper-Dateien weißt du nicht mehr, woher format_price gerade kommt. Zum Testen brauchst du meist einen Controller- oder Systemtest, der den halben Stack mitlädt, vom Routing bis zum Layout.

Spätestens wenn dieselbe Karte an mehreren Stellen mit leicht anderem Inhalt erscheint, lohnt sich der Umstieg. Konkret heißt das: Die Darstellung wird zu einem Objekt mit klarer Schnittstelle.

Du musst dafür nicht die ganze App umbauen. Partials und Komponenten vertragen sich in derselben View, und eine bestehende Seite kann Stück für Stück umziehen. Das Layout bleibt sowieso, wie es ist:

<%# app/views/layouts/application.html.erb %>
<!DOCTYPE html>
<html>
  <head>
    <title><%= content_for(:title) || "Shop" %></title>
    <%= stylesheet_link_tag "application" %>
  </head>
  <body>
    <%= yield %>
  </body>
</html>

Welches Layout gilt, kannst du weiterhin im ApplicationController festlegen. Komponenten erzeugen nur den Inhalt, der an der Stelle von yield landet. Sinnvoll ist eine einfache Regel im Team: Neues UI mit mehr als einer Variante wird Komponente, alte Partials werden erst angefasst, wenn ohnehin eine Änderung ansteht.

So funktioniert eine View-Komponente

Das Prinzip ist bei allen drei Gems gleich. Eine Ruby-Klasse nimmt im Konstruktor genau die Daten an, die sie braucht. Private Hilfen der Klasse bereiten Werte auf. Eine ERB-Datei oder reiner Ruby-Code erzeugt daraus HTML. Von außen rufst du die Komponente mit render auf.

Was du damit gewinnst:

  • Fehlt ein Pflichtargument, wirft Ruby sofort einen ArgumentError, schon beim ersten Aufruf im Test.
  • Logik, Markup und CSS-Klassen liegen zusammen, Hilfsmethoden bleiben privat und tauchen in keiner anderen View auf.
  • Du renderst die Komponente ohne Request und prüfst die Ausgabe in Millisekunden.
Schematische Darstellung: eine Komponentenklasse mit Template wird in mehrere Views eingebunden

Eine Klasse, ein Template, beliebig viele Einsatzorte: das Grundprinzip jeder View-Komponente.

Cells, ViewComponent und Phlex im Überblick

Die Tabelle fasst die Unterschiede zusammen. Die Versionsangaben stammen von rubygems.org (Stand November 2026).

Kriterium Cells ViewComponent Phlex
Ansatz View-Model mit Template, aus dem Trailblazer-Umfeld Komponentenklasse plus Template, von GitHub entwickelt HTML komplett in Ruby-Methoden
Templates ERB, Haml oder Slim über Zusatz-Gems ERB, Haml, Slim oder Inline-call keine Template-Datei
Performance-Anspruch kein eigenes Benchmark-Versprechen laut Doku ca. 2,5-mal schneller als Partials Projekt nennt Tempo als Kernziel
Testbarkeit Cell direkt aufrufen, String prüfen render_inline mit Capybara-Matchern Komponente zu String rendern
Aktueller Stand cells 4.1.8 (11/2024) 4.23.0 (11/2026), Long-Term Support phlex 2.4.1, phlex-rails 2.4.0
Einsatzfall Bestandsprojekte mit Trailblazer Standard für neue Rails-Apps Teams, die Markup lieber in Ruby schreiben

Cells

Cells ist der älteste der drei Ansätze. Eine Cell erbt von Cell::ViewModel, die Vorlage wird standardmäßig unter app/cells/<name>/show.erb abgelegt. Für Rails brauchst du cells-rails und eine Template-Engine wie cells-erb. Achtung beim Gem-Namen: trailblazer-cells wurde seit 2016 nicht mehr veröffentlicht, aktiv ist das Paket cells.

# app/cells/comment_cell.rb
class CommentCell < Cell::ViewModel
  property :body
  property :author_name

  def show
    render
  end

  private

  def posted_at
    model.created_at.strftime("%d.%m.%Y")
  end
end
<%# app/cells/comment/show.erb %>
<article class="comment">
  <p><%= body %></p>
  <footer><%= author_name %>, <%= posted_at %></footer>
</article>

In der View genügt <%= cell(:comment, @comment) %>. Das property reicht Attribute des übergebenen Models an das Template durch, das Model selbst erreichst du über model. Rendern kannst du eine Cell in Views, Controllern und Mailern. Für neue Projekte spricht wenig dafür. Die letzte Version 4.1.8 stammt vom November 2024.

ViewComponent

ViewComponent ist heute der verbreitetste Weg. Klasse und ERB-Datei werden nebeneinander im Verzeichnis app/components abgelegt. Laut Projektseite ist das Gem seit Version 4 im Long-Term Support und gilt als funktional abgeschlossen. Für dich heißt das: wenig Bewegung in der API, dafür stabile Upgrades.

# app/components/comment_component.rb
class CommentComponent < ViewComponent::Base
  def initialize(comment:)
    @comment = comment
  end

  private

  def posted_at = @comment.created_at.strftime("%d.%m.%Y")
end
<%# app/components/comment_component.html.erb %>
<article class="comment">
  <p><%= @comment.body %></p>
  <footer><%= @comment.author_name %>, <%= posted_at %></footer>
</article>

Gerendert wird mit <%= render CommentComponent.new(comment: @comment) %>. Mit Slots baust du zusammengesetzte Komponenten. Ein Beispiel: Eine CardComponent deklariert renders_one :header und renders_many :actions. In der View übergibst du die Teile dann mit card.with_header { "Bestellung 4711" } und card.with_action. Die Komponente entscheidet selbst, wo der Header landet und ob überhaupt ein Footer nötig ist. Das alte Partial brauchte dafür noch zwei defined?-Abfragen.

Phlex

Phlex verzichtet auf die ERB-Datei. Jedes HTML-Tag wie div oder form ist eine Ruby-Methode, und view_template erzeugt daraus das Markup. Das fühlt sich anfangs fremd an. Dafür kannst du Komponenten mit normalen Ruby-Mitteln wie Vererbung und Modulen zerlegen. Ein Teil des Markups, den du auslagern willst, wird einfach zu einer privaten Hilfe in derselben Klasse.

# app/components/comment.rb
class Components::Comment < Components::Base
  def initialize(comment:)
    @comment = comment
  end

  def view_template
    article(class: "comment") do
      p { @comment.body }
      footer { "#{@comment.author_name}, #{posted_at}" }
    end
  end

  private

  def posted_at = @comment.created_at.strftime("%d.%m.%Y")
end

Die Basisklasse Components::Base legt der Generator von phlex-rails an. Rails-Helper wie link_to stehen in Phlex nicht automatisch bereit. Du bindest sie einzeln als Modul ein, zum Beispiel mit include Phlex::Rails::Helpers::LinkTo. Das ist etwas mehr Schreibarbeit, macht aber sichtbar, welche Helper eine Komponente wirklich nutzt.

Komponenten mit Hotwire und Turbo kombinieren

Interaktive Widgets mit Zustand brauchten früher eine eigene AJAX-Schicht: JavaScript holt HTML vom Server und tauscht es von Hand aus. Mit Turbo Frames fällt das weg. Die Komponente rendert sich selbst in ein turbo_frame_tag, und der Controller liefert beim nächsten Klick einfach dieselbe Komponente neu aus.

<%# app/components/cart_component.html.erb %>
<%= turbo_frame_tag dom_id(@cart) do %>
  <p><%= @cart.items.count %> Artikel</p>
  <%= button_to "Leeren", cart_path(@cart), method: :delete %>
<% end %>
class CartsController < ApplicationController
  def destroy
    @cart = Cart.find(params[:id])
    @cart.items.destroy_all
    render CartComponent.new(cart: @cart)
  end
end

Turbo ersetzt nur den Frame mit derselben ID, der Rest der Seite bleibt stehen. Möglich ist das, weil render im Controller jedes Objekt annimmt, das die Methode render_in kennt. ViewComponent bringt sie mit, Phlex über phlex-rails. Für Updates an mehreren Stellen gleichzeitig nutzt du Turbo Streams und renderst die Komponente im Block von turbo_stream.replace.

Ein guter Kandidat für dieses Muster ist ein Formularfeld mit Validierung: Die Komponente zeigt Label, Eingabe und Fehlermeldung, der Controller rendert sie nach dem Absenden mit den Fehlern neu. Ein form_with im Frame reicht dafür, eigenes JavaScript brauchst du nicht.

<%# app/components/form_field_component.html.erb %>
<div class="form-field">
  <%= @form.label @attribute %>
  <%= @form.text_field @attribute %>
  <% @form.object.errors.full_messages_for(@attribute).each do |message| %>
    <p class="form-error"><%= message %></p>
  <% end %>
</div>

Im Formular rufst du das Feld mit render FormFieldComponent.new(form: f, attribute: :email) auf. Scheitert die Validierung in der create-Action, rendert der Controller das Formular mit HTTP-Status 422 neu. Turbo tauscht dann nur den Frame aus. Die Felder zeigen wieder, was eingegeben wurde, weil das Formular mit dem Model neu gerendert wird.

Der Haken: Der Zustand lebt auf dem Server. Für ein Dropdown, das nur auf- und zuklappt, ist ein Request zu viel. Das erledigt ein kleiner Stimulus-Controller besser.

Komponenten testen

In der Praxis merkst du den Unterschied hier am stärksten. Ein Komponententest läuft ohne Routing und ohne Datenbank-Request, wenn du ein ungespeichertes Model übergibst.

# test/components/comment_component_test.rb
class CommentComponentTest < ViewComponent::TestCase
  def test_renders_author_and_date
    comment = Comment.new(body: "Hallo", author_name: "Kim",
                          created_at: Time.zone.local(2026, 11, 9))
    render_inline(CommentComponent.new(comment: comment))

    assert_selector "article.comment p", text: "Hallo"
    assert_text "Kim, 09.11.2026"
  end
end

Bei Phlex rufst du Components::Comment.new(comment: comment).call auf und bekommst einen String mit HTML, den du mit Nokogiri oder einem einfachen assert_includes prüfst. Eine Cell testest du ähnlich direkt über cell(:comment, comment).(). ViewComponent bietet zusätzlich Previews unter test/components/previews, die du im Browser durchklicken kannst. Für ein Team mit Designern ist das oft das stärkste Argument.

Infografik: Entscheidungshilfe zwischen Cells, ViewComponent und Phlex in vier Fragen

Vier Fragen führen zur passenden Komponentenbibliothek.

Checkliste: Welche Bibliothek passt zu deinem Projekt?

Für neue Projekte würde ich ViewComponent nehmen: Die Doku ist gut, die Tests sind schnell geschrieben, und die meisten Rails-Entwickler kennen das Muster schon. Phlex ist die bessere Wahl, wenn dein Team ERB ohnehin als Ballast empfindet. Cells lohnt sich vor allem dort, wo es schon läuft.

Geh diese Fragen der Reihe nach durch:

  1. Gibt es schon Cells im Code? Dann bleib dabei und migriere nur neue Teile, wenn es Gründe gibt.
  2. Schreibt dein Team Markup gern in ERB? Dann ViewComponent.
  3. Soll das HTML vollständig in Ruby stehen, mit Vererbung statt Partials? Dann Phlex.
  4. Brauchst du einen Katalog zum Durchklicken? Die Previews von ViewComponent sind eingebaut, bei Phlex und Cells musst du so etwas selbst bauen.
  5. Hast du weniger als zehn wiederkehrende UI-Bausteine? Dann reichen Partials mit klaren locals vielleicht noch eine Weile.

Fang mit der Komponente an, die am häufigsten kopiert wird. Meist ist das ein Button oder eine Karte. Mehr Artikel zu Ruby und Rails findest du in der Rubrik Technik.

Click to rate this post!
[Total: 0 Average: 0]

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert