How to use ViewComponents in Ruby on Rails

Updated: 

Discover how to use ViewComponents in Ruby on Rails for cleaner, more maintainable UI code. Learn the benefits over traditional partials and see practical examples."

Alex Sinelnikov

Alex Sinelnikov

Follow me on Twitter for more interesting content.
Since the beginning of ages we were using partials in our Ruby on Rails apps. I think everyone remember good old app/views/shared folder where many of us kept parts of our UI. It was right approach when you wanted to reuse some parts of html. The only problem with it - you can't really put logic there. Well you definitely can but you don't want your html file to be loaded with various if-else clauses.
That's where https://viewcomponent.org comes to stage. Essentially it's plain ruby object which contains some logic and html file which uses objects and logic from ruby class. Here's dropdown component I created some time ago.

# app/components/dropdown_component.rb
class DropdownComponent < ViewComponent::Base
  def initialize(form:, field_name:, options:, label: nil, placeholder: "Select an option")
    @form = form
    @field_name = field_name
    @options = options
    @label = label || field_name.to_s.humanize
    @placeholder = placeholder
  end

  private

  attr_reader :form, :field_name, :options, :label, :placeholder
end


followed by same name html file
<!-- app/components/dropdown_component.html.erb -->
<div x-data="{ open: false, selected: '' }" class="relative">
  <%= form.label field_name, label, class: "block text-sm font-medium text-gray-700 mb-1" %>
  <div @click.away="open = false" class="relative">
    <button @click="open = !open" type="button" 
            class="relative w-full bg-white border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
      <span x-text="selected || '<%= placeholder %>'" class="block truncate"></span>
      <span class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
        <svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
          <path fill-rule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" />
        </svg>
      </span>
    </button>
    <div x-show="open" 
         x-transition:enter="transition ease-out duration-100" 
         x-transition:enter-start="transform opacity-0 scale-95" 
         x-transition:enter-end="transform opacity-100 scale-100" 
         x-transition:leave="transition ease-in duration-75" 
         x-transition:leave-start="transform opacity-100 scale-100" 
         x-transition:leave-end="transform opacity-0 scale-95" 
         class="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
      <% options.each do |option| %>
        <div @click="selected = '<%= option[:label] %>'; open = false" 
             class="cursor-default select-none relative py-2 pl-3 pr-9 hover:bg-indigo-600 hover:text-white">
          <%= option[:label] %>
        </div>
      <% end %>
    </div>
  </div>
  <%= form.hidden_field field_name, 'x-model': 'selected' %>
</div>


and then, in order to use it simply call following anywhere in your html.
<%= render(DropdownComponent.new(
  form: form,
  field_name: :category,
  options: Category.all.map { |c| { value: c.id, label: c.name } }
  label: 'Select a category',
  placeholder: 'Choose a category'
)) %>


Having just 1 component won't make much difference, but when you create/migrate majority of your components - it will start to save you time when start reusing components. Another benefit - is when you decide to restyle your app, you'll have to do it only once - in your components.
You are not limited to just classes. You can also add JS code or anything else that handles your UI. I usually work with Alpine.JS and have components such as Dropdown, etc, where everything UI handled inside and the code above is a great example to show how its used.
Once you have many components you can add https://github.com/lookbook-hq/lookbook which is as name states - lookbook for your components. Instead of going to component sources, you can predefine a set of variants and just copy paste them when you need, just like for any UI kit.
Conclusion

While both partials and ViewComponents serve the purpose of creating reusable UI elements, ViewComponents offer significant advantages in terms of organization, testability, and performance. By using ViewComponents, you can create more maintainable and scalable view layers in your Rails applications and using Lookbook you can save yourself time by copy pasting instead of writing from scratch.
Follow me on Twitter for more interesting content.