Skip to content

Component Philosophy — Phlex vs ViewComponent vs Partials

Lesson 2 — Component philosophy: Phlex vs ViewComponent vs partials

The component landscape

Once you accept that you want encapsulated UI components, you have choices. Rails itself doesn’t provide a component abstraction, but the ecosystem has filled the gap in different ways. Understanding the options — and their tradeoffs — will help you make a confident, informed choice in favour of Phlex.

The three approaches worth comparing:

  1. ERB partials (built-in, no gem required)
  2. ViewComponent (GitHub’s component library, the established choice)
  3. Phlex (pure Ruby components, the modern choice)

ERB partials — the baseline

We covered the problems in Lesson 1. But partials do have genuine advantages:

  • Zero setup — they’re part of Rails
  • Familiar to every Rails developer
  • Great tooling support (ERB syntax highlighting, language servers, etc.)
  • Fast enough for most applications
  • Easy to migrate to incrementally

The verdict: partials are fine for simple, stable UI fragments. A _flash_messages.html.erb or a _footer.html.erb doesn’t need to become a component. The pain arrives when you need parameterised, reusable, testable UI — and that’s where both ViewComponent and Phlex shine.


ViewComponent — the established choice

ViewComponent was built by GitHub and has been battle-tested at enormous scale. It introduces a Ruby class alongside each ERB template:

1
2
3
4
5
6
7
# app/components/post_card_component.rb
class PostCardComponent < ViewComponent::Base
  def initialize(post:, show_author: false)
    @post = post
    @show_author = show_author
  end
end
<%# app/components/post_card_component.html.erb %>
<div class="post-card">
  <h2><%= @post.title %></h2>
  <% if @show_author %>
    <p>By <%= @post.author.name %></p>
  <% end %>
</div>

This is a genuine improvement over raw partials. The interface is explicit (the initialize method), instance variables are private to the component, and the component can be unit-tested cleanly.

What ViewComponent does well:

  • Strong Rails integration — it works exactly like a view
  • Sidecar files: the .rb and .html.erb sit next to each other
  • Established gem with strong community and GitHub backing
  • Stimulus controller sidecar support
  • Preview system for component development in isolation

Where ViewComponent has friction:

  • You always need two files — the Ruby class and the ERB template
  • The template is still ERB, so all the ERB pain (string concatenation for dynamic classes, no IDE type checking on variables passed to the template, etc.) is still present
  • Composition and slots are possible but feel bolted on
  • The file-per-component discipline means extracting small sub-components has real overhead
  • Testing requires a ViewComponent-specific test helper

Phlex — the modern choice

Phlex takes a different position: what if the component and its template were the same Ruby class?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# app/components/post_card.rb
class Components::PostCard < Components::Base
  def initialize(post:, show_author: false)
    @post = post
    @show_author = show_author
  end

  def view_template
    div(class: "post-card") do
      h2 { @post.title }
      p { "By #{@post.author.name}" } if @show_author
    end
  end
end

No ERB file. No separate template. The HTML structure is described using Ruby methods that map directly to HTML tags. div, h2, p — they’re just method calls that take blocks.

This has a cascade of consequences that matter enormously in practice.


Why “everything is a Ruby class” changes everything

Explicit interfaces, for free.
Because a Phlex component is just a Ruby class with an initialize method, its interface is exactly what Ruby gives you: keyword arguments with types, defaults, and required/optional semantics. Your IDE understands it. RubyLSP can autocomplete it. RuboCop can lint it. This is not possible with ERB partials or ViewComponent templates.

Composition is natural.
In Ruby, you compose objects. In Phlex, composing components is the same thing:

1
2
3
4
5
6
def view_template
  render Components::Card.new do
    render Components::PostCard.new(post: @post)
    render Components::TagList.new(tags: @post.tags)
  end
end

There’s no special slot API to learn. You just render components inside other components, the same way you nest Ruby objects.

Kits make component libraries elegant.
Phlex v2 introduces Kits — modules that expose a collection of components with a clean call syntax:

1
2
3
4
5
6
7
module Components
  extend Phlex::Kit
end

# Now anywhere in your views:
render Components::PostCard(post: @post)
render Components::Button("Save", variant: :primary)

This feels like a design system DSL, but it’s just Ruby modules.

Testing is trivial.
Because a Phlex component is a plain Ruby object, testing it requires nothing special:

1
2
3
4
5
6
7
8
test "post card shows author when requested" do
  post = Post.new(title: "Hello", author: User.new(name: "Alice"))
  component = Components::PostCard.new(post: post, show_author: true)

  render_inline(component)

  assert_text "Alice"
end

No view context mocking, no partial rendering setup. Instantiate, render, assert.

Performance.
Phlex renders HTML at approximately 1.4 GB/s per core on modern hardware. It doesn’t slow down as you extract more components — each component call is a method call, not a file load. For most applications this doesn’t matter much, but it’s worth knowing there’s no performance penalty for aggressive component extraction.

Tailwind CSS is a natural fit.

Tailwind’s utility-first approach is designed for component-based development — but it has a serious problem in a traditional Rails app. Utility classes end up scattered across ERB templates, partials, and helpers alongside the markup. A _post_card.html.erb with class="flex items-center gap-3 p-4 rounded-lg border bg-white shadow-sm hover:shadow-md" repeated across a dozen templates is as hard to maintain as the HTML around it. Changing the card’s appearance means hunting down every partial that renders one.

With Phlex, every component owns its styles. The Tailwind classes for a Button live in Components::Button and nowhere else. Changing the button’s appearance means changing one file. Adding a new variant means adding one entry to a constants hash. The component is the design system — not a convention spread across dozens of files.

This is why Tailwind and Phlex are a natural pairing. They share the same philosophy: co-locate everything that belongs together, make the single source of truth explicit, and eliminate the implicit coupling that makes large codebases hard to change.

In Module 7 we take this further with CSS design tokens — custom properties that let you retheme the entire Phlex::UI library by changing values in one place, with full dark mode support.


The honest comparison

Partials ViewComponent Phlex
Files per component 1 2 1
Explicit interface No Yes (Ruby) Yes (Ruby)
Template language ERB ERB Ruby
IDE support Partial Good Excellent
Unit testable Awkward Yes Yes
Composition model Manual Slots API Native Ruby
Rails helper access Implicit Via helpers Via adapters
Learning curve None Low Low–Medium
Incremental adoption Yes Yes Yes

The case for ViewComponent over Phlex is mostly familiarity with ERB and the existing ecosystem. If your team is deeply comfortable with ERB and has an existing ViewComponent library, the migration cost may not be worth it.

The case for Phlex is everything else: a single file per component, a purely Ruby mental model, natural composition, excellent testability, and the kind of IDE support that makes refactoring feel safe.


Phlex and the Rails ecosystem

One concern developers often raise: does Phlex play nicely with the rest of Rails?

The answer is yes, deliberately so. Phlex was designed to work alongside ERB — you can render a Phlex component from an ERB template and vice versa. This means adoption can be incremental. You don’t rewrite your app; you introduce Phlex components one at a time, in the parts of the UI where the complexity justifies it.

Phlex also ships adapters for all major Rails helpers: link_to, image_tag, form_with, route helpers, content_for, and more. Turbo and Stimulus work exactly as you’d expect — Phlex just generates HTML, and Turbo/Stimulus don’t care where the HTML came from.

In this tutorial series, we’ll use Phlex exclusively from Module 4 onwards — no ERB views at all. This is the “all in” approach. It’s not the only valid approach, but it’s the best way to really understand what Phlex makes possible.


The mental model to carry forward

Before we write a single line of Phlex, internalise this:

A Phlex component is a Ruby object that knows how to render itself as HTML.

That’s it. Everything else — tags, attributes, composition, testing, kits, layouts — is a consequence of taking that idea seriously.

In the next module, we’ll start building that intuition with the Phlex HTML DSL, before touching Rails at all.


Module 1 summary

  • ERB partials work well for simple, stable fragments. They break down when you need parameterised, reusable, testable UI.
  • The four-way tension between partials, helpers, layouts, and instance variables creates implicit coupling that resists refactoring and testing.
  • ViewComponent solves the interface and testing problems but keeps ERB as the template language.
  • Phlex solves all of them: one file, explicit Ruby interface, natural composition, trivial testing.
  • Phlex is fully compatible with Rails — adoption can be incremental, and all Rails helpers are supported.
  • The core mental model: a component is a Ruby object that renders itself as HTML.