From Partials (and Helpers) to Embracing ViewComponent in Rails

Historically Rails’ answer to the view layer was a mix of views, partials and helpers. And when you start out with a somewhat-basic app, this will work just fine.

Maybe you are feeling the pain of using Rails’ partials and helpers or maybe you want to make sure you never get there and want to move to ViewComponent already.

In either case you have some (or many) partials (that uses one or many methods from helpers).

Why use ViewComponent? I’ve written a full article on why you should choose ViewComponent. It covers everything from the pros, the cons and advanced features like slots.

What steps to go through when you want to take the plunge and move from partials to ViewComponent?

Start with one partial

Depending on the size of your app you might have many partials already, and it might seem like a too big a task to pull off. Just remember that a big promise of ViewComponent is speed-increase (including testing). So over time, you and your team could see a productivity boost!

But whatever size you’re at, start with one partial. Use it as a playground. Ideally a partial that:

  • uses a helper method;
  • uses a collection.

The reason is I want helpers to be only “global” (like some of the Rails Designer View Helpers). And a collection would be ideal as it generally needs a bit more work.

Example

Enough theory! Let’s look an an example:

<div class="messages-list">
  <h2>Messages (<⁠%= @messages.count %>)</h2>

  <ul class="flex flex-col gap-y-3">
    <⁠% @messages.each do |message| %>
      <li class="message-item">
        <div class="flex items-center justify-between">
          <strong><⁠%= message.sender.name %></strong>

          <small><⁠%= format_message_timestamp(message.created_at) %></small>
        </div>

        <p><⁠%= truncate_message_body(message.body) %></p>

        <div class="flex items-center justify-between">
          <⁠%= message_status_badge(message.status) %>

          <⁠%= link_to "View", message_path(message), class: "btn btn--primary" %>
        </div>
      </li>
    <⁠% end %>
  </ul>
</div>

It’s your typical Rails partial to list messages. As you can see it uses a few helpers too:

module MessagesHelper
  def format_message_timestamp(timestamp)
    timestamp.strftime("%b %d, %Y at %I:%M %p")
  end

  def truncate_message_body(body)
    truncate(body, length: 100, separator: " ")
  end

  def message_status_badge(status)
    case status
    when "read"
      content_tag(:span, "Read", class: "badge badge-success")
    when "unread"
      content_tag(:span, "Unread", class: "badge badge-warning")
    else
      content_tag(:span, "Unknown", class: "badge badge-secondary")
    end
  end
end

The beautiful thing about ViewComponent is that it can work without much work. Let’s create a component first (this assuming you have it added to your app): rails g component MessagesList. By default this creates two files:

  • app/components/messages_list_component.rb
  • app/components/messages_list_component.html.erb

The former is where typically all methods go (eg. format_message_timestamp and message_status_badge from the MessagesHelpers. The latter where your erb code goes.

Let’s set up the app/components/messages_list_component.rb first.

class MessagesListComponent <  ViewComponent::Base
  def initialize(message:)
    @message = message
  end
end

The app/components/messages_list_component.html.erb:

<li class="message-item">
  <div class="flex items-center justify-between">
    <strong><⁠%= @message.sender.name %></strong>

    <small><⁠%= format_message_timestamp(@message.created_at) %></small>
  </div>

  <p><⁠%= truncate_message_body(@message.body) %></p>

  <div class="flex items-center justify-between">
     <⁠%= message_status_badge(@message.status) %>

    <⁠%= link_to "View", message_path(@message), class: "btn btn--primary" %>
  </div>
</li>

That looks the same as the partial! It is indeed just a copy/paste!

How to render this component in a view? Like so:

<div class="messages-list">
  <h2>Messages (<⁠%= @messages.count %>)</h2>

  <ul class="flex flex-col gap-y-3">
    <⁠%= render(MessagesListComponent.with_collection(@messages)) %>
  </ul>
</div>

This uses ViewComponent’s collections feature.

Let’s now move to the helpers. Because they are “scoped” in a MessagesHelper module, they are by no means scoped in the real sense of the world. Anywhere in your app, you can use truncate_message_body. Which could turn into nasty bugs!

So move the methods from the MessagesHelper and delete that file! 🗑️

class MessagesListComponent <  ViewComponent::Base
  def initialize(message:)
    @message = message
  end

  def timestamp
    @message.created_at.strftime("%b %d, %Y at %I:%M %p")
  end

  def truncated_body
    truncate(@message.body, length: 100, separator: ' ')
  end

  def status_badge
    case @message.status
    when "read"
      content_tag(:span, "Read", class: "badge badge-success")
    when "unread"
      content_tag(:span, "Unread", class: "badge badge-warning")
    else
      content_tag(:span, "Unknown", class: "badge badge-secondary")
    end
  end
end

With that done, let’s update the app/components/messages_list_component.html.erb:

<li class="message-item">
  <div class="flex items-center justify-between">
    <strong><⁠%= @message.sender.name %></strong>

    <small><⁠%= timestamp %></small>
  </div>

  <p><⁠%= truncated_body %></p>

  <div class="flex items-center justify-between">
     <⁠%= status_badge %>

    <⁠%= link_to "View", message_path(@message), class: "btn btn--primary" %>
  </div>
</li>

That’s it. Your first ViewComponent done! 🌟 The component now uses its own methods instead of the globally available helper methods.

Use ViewComponent for new UI elementscomponents only

Once you get a good feel for the first new ViewComponent. Create that Pull Request! Share it with your team. See what they have to say. Any improvements to be made? What concerns do they have?

In general it’s good practice to document as much as possible on certain decisions when you introduce a new technology. Provide extra articles, resources and so on. Enough reading material on this site!

Once you got the okay from your team. Don’t move every partial over to ViewComponent. In fact not every partial might be moved over at all.

When to move a partial over?

  • it uses methods from helpers that are only concerned with that element;
  • it uses a lot of variables;
  • it needs solid testing (eg. admin controls).

For example, almost all the apps I built, use a views/shared/_head.html.erb partial. It has the typical head-element for every layout. There’s no need for any variables, testing or whatever. So I keep it as a partial.

And then over time, when you and your team become more comfortable with ViewComponent start plucking away at your existing partials and move each of them over to ViewComponent. Keep those PR’s small!

Reach out if you need an extra pair of hands with this process.

Get UI & Product Engineering Insights for Rails Apps (and product updates!)

Published at . Last updated at . Have suggestions or improvements on this content? Do reach out.

UI components for Ruby on Rails apps

$ 129 one-time
payment

Get Access
  • One-time Payment

  • Access to the Entire Library

  • Built for Ruby on Rails (inc. Rails 8)

  • Designed with Tailwind CSS and Enhanced with Hotwire

  • Updates for 12 months