From Partials (and Helpers) to Embracing ViewComponent in Rails

RMAG news

This article was originally published on Rails Designer

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 starting to feel the pain of using Rails’ partials and helpers already 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 already have many partials already, and it might feel 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 might 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.

🎨 Your Rails app’s UI not up to today’s standard? Need some professionally-designed UI components ready to copy/paste into your app? Check out Rails Designer.

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 a list of messages. As you can see it uses a few helper methods 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 helper’s methods. 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><⁠%= truncates_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 to 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!

Please follow and like us:
Pin Share