Tips for Using ViewComponents in Rails
One of the concepts that needs to be added in Ruby on Rails is the concept of UI components. We have helpers and partials. However, as the project grows, it becomes very, very messy. Plus, partials are surpassingly slow. 🫥
To solve this problem at Angry Building, I'm using ViewComponent gem from Github.
I’m going to cover today
I gave a talk about ViewComponent (slides and video). This post is largely based on this post.
What is ViewComponent?
ViewComponent is a gem that allows you to create UI components encapsulated in Ruby classes. It was extracted from Github and is also used by Gitlab.
Here is an example of “FieldsetComponent".
This is how this component is going to be rendered:
<%= render FieldsetComponent.new(fieldset, title: "Bank account") do %>
<%= form.input :bank %>
<%= form.input :iban %>
<%= form.input :bic %>
<% end %>
The component has two parts—a Ruby class and an ERB template
# app/components/fieldset_component.rb
class FieldsetComponent < ViewComponent::Base
attr_reader :title
def initialize(title:)
@title = title
end
end
<!-- app/components/fieldset_component.html.erb -->
<fieldset class="c-box p-4">
<legend class="c-box px-3 py-1">
<%= title %>
</legend>
<div class="space-y-6">
<%= content %>
</div>
</fieldset>
…and that's it. Pretty simple 🤩
The official site of ViewComponent has excellent documentation.
Consider integrating Lookbook with ViewComponent. Its setup is just a "gem install" and adding a route.
It can't even be compared to the complexity of integrating Playbook into a Next.js project. 😜
When should I use ViewComponent?
I still use helper and partials; ViewComponent is just a new tool. Here are my rules about how to use it
I use a view component when
I am considering extracting a partial that will be used in 2+ controllers
considering extracting a view helper that generates HTML
have complicated deep nested if-elsif-else (domain in view)
copy a lot of logic around
have to connect with JavaScript
I don't use ViewComponent when:
a partial is only used in one controller (example: _form.html.erb)
view helper, which is a simple pure function (example: format_money)
there is a lot of HTML on one single page - leave it there 🤷♂️
Tips
Can't name a post "Tips for" if there aren't some actionable tips in the post 🧐
Have a “component” helper
Have “ApplicationComponent
Aliasing slots
Editor enhancements
Lets break those down:
1/ Have a "component" helper.
<%= render FieldsetComponent(title: 'title') %>
// becomes
<%= component :field_set, title: 'title' %>
It is a small change, but it makes components feel more "at home" in Rails' view. Here is a gist of its implementation 👉 link
2/ Have "ApplicationComponent" base class similar to "ApplicationRecord", "ApplicationController".
Mine has only two methods, but I still find it useful.
3/ Alias "slots" to make code more readable
ViewComponent has the concept of slots. It allows you to inject content inside your component. Very useful. However, the slots methods are prefixed with "with_," which I found less readable, so I often hide the fact I'm using a slot.
class StatsComponent < ApplicationComponent
renders_many :numbers, StatsNumberComponent
alias number with_number
end
// I find this less readable
<%= component :stats do |c| %>
<% c.with_number :balance %>
<% end %>
// than the alias version
<%= component :stats do |c| %>
<% c.number :balance %>
<% end %>
4/ Have switch to alternative shortcut
I use vim-projectionist and it allows me to define shortcuts to switch between component class and template. Very handy. 😎
The builder pattern
One of the more "advanced" patterns I started doing with ViewComponent is the "builder pattern". It is similar to how Rails FormBuilder looks like. You are calling methods of the ViewComponent to define how to display the component.
Here is an example of this pattern for "FilterFormComponent"
<%= component :filter_form, params: params do |form| %>
<% form.search :query %>
<% form.select :user_id, options: @search.user_options %>
<% form.select :source, options: @search.source_options %>
<% form.date_range :date %>
<% form.select :kind, options: @search.kind_options %>
<% end %>
In my talk, I go through even more powerful example of the builder pattern in my TableComponent, where you use the builder pattern to define rules for each cell:
<%= component :table, @search.results do |table| %>
<% table.record :name, :itself %>
<% table.record :apartment %>
<% table.number :document do |record| %>
<% record.documents.each do |document| %>
<%= link_to document.display_number, document_path(document) %>
<% end %>
<% end %>
<% table.record :cashier, :user %>
<% table.date :date %>
<% table.money :cash_reserve_amount %>
<% table.money :wallet_amount %>
<% table.money :total_amount %>
<% table.column :kind do |record| %>
<%= component :transaction_kind_badge, record %>
<% end %>
<% table.actions do |record| %>
<%= button_details transaction_path(record) %>
<% end %>
<% end %>
You can check the code for this 👉 here.
Why not use Phlex?
Phlex is another way to add UI components in Rails applications. It also allows you to put your components in Ruby classes.
The main difference from ViewComponent is that it uses a Ruby DSL to define the HTML for the component.
It is awesome, and if ViewComponent hadn't existed, I would have used it.
However, I prefer to use ViewComponent because
I prefer ERB over the DSL. I don't use components for everything; I only use them for reusable code. Thus, I often move code between ERB and DSL, and converting it from and to the DSL will slow me down. Or if I copied the code from ChatGPT, it will slow me down. Plus, DSL is something else I need to learn.
ViewComponent is backed and used by both Github and Gitlab, which gives me confidence in the project's longevity.
Those same reasons might be why someone else will choose Phlex over ViewComponent—context matters. 😅
Conclusion
For even more tips and tricks about ViewComponent, my talk is here with a video. 🙈
Choosing ViewComponent was one of the best architecture decisions I made for ViewComponent. I'll definitely use it again if I'm starting a new Rails project (where Rails renders views).
ViewComponent should be built into Rails, too bad DHH is not a fan. 🤷♂️
If you have any questions or comments, you can ping me on Threads, LinkedIn, Mastodon, Twitter or just leave a comment below 📭