Refactoring my Ruby on Rails front-end with ViewComponent

Refactoring my Ruby on Rails front-end with ViewComponent

Content of this post

Introduction

Some time ago i created a personal project using Ruby on Rails, it's a full-stack monolith and has different contexts, partials and scopes inside the views, and even using Slim it was starting to become a mess. Well, it's not that uncommon to hear this kind of situation in Rails applications, that's why we have good solutions for this, for example:

  • Helpers
  • Decorators
  • Facades
  • Presenters

I've already tried these options in previous projects i worked on and was searching something new, something modern and something that would help me to write more tests to my different contexts in the front-end of my application, that's when I found the gem ViewComponent.

These are some information we can find description:

A framework for creating reusable, testable & encapsulated view components, built to integrate seamlessly with Ruby on Rails.

ViewComponents are Ruby objects used to build markup. Think of them as an evolution of the presenter pattern, inspired by React.

And another information caught my attention:

Based on several benchmarks, ViewComponents are ~10x faster than partials in real-world use-cases.

The primary optimization is pre-compiling all ViewComponent templates at application boot, instead of at runtime like traditional Rails views.

So, it uses Ruby objects, it's inspired by React components, it's easy to test and it performs 10x better than common partial rendering in Rails, that's awesome and it's exactly what i was looking for.

So, let's try it!

Installing the ViewComponent Gem

Add this to your Gemfile..

gem "view_component", require: "view_component/engine"

...and then run in your console:

bundle install

Creating components

To create new components the ViewComponent gem provides a generator, in this case let's suppose we are creating a Navbar component:

bin/rails generate component Navbar user

This command generate some new files inside the app/component folder and inside the spec/components folder if you are using RSpec to write your tests. In this case we have a Ruby file navbar_component.rb and a Erb file navbar_component.erb.html (it could be Slim or Haml).

We have our initialize method that receives an user as argument:

# app/components/navbar_component.rb
class NavbarComponent < ViewComponent::Base
  def initialize(user:)
    @user = user
  end
end

Our markup with a conditional to check if the user is present and render a link depending on it:

# app/components/navbar_component.html.erb
<navbar>
  <ul>
    <% if @user.present? %>
      <li><%= link_to "My account", my_account_url %></li>
    <% end %>
    <li><%= link_to "Home", home_url %></li>
  </ul>
</navbar>

And finally we can render our component, in this case it's being rendered inside our application.html.erb file:

# app/views/layouts/application.html.erb
render(NavbarComponent.new(user: current_user))
<%= yield %>

Component methods

The public methods inside our NavbarComponent.rb file can be accessed directly in our navbar_component.html.erb, for example, let's create a simple method to check if the user is present:

# app/components/navbar_component.rb
class NavbarComponent < ViewComponent::Base
  def initialize(user:)
    @user = user
  end

  def logged_user?
    user.present?
  end
end

I'm using a logged_user? method name, but it can be any other name. In our html.erb file now we can check the presence of user with our new method:

# app/components/navbar_component.html.erb
<navbar>
  <ul>
    <% if logged_user? %>
      <li><%= link_to "My account", my_account_url %></li>
    <% end %>
    <li><%= link_to "Home", home_url %></li>
  </ul>
</navbar>

Testing with RSpec

Finally, let's write some tests!

However, before we write our tests, we have to configure our RSpec to include the ViewComponent helper methods. Inside our rails_helper.rb file or a spec/support file:

# spec/rails_helper.rb
require "view_component/test_helpers"

RSpec.configure do |config|
  config.include ViewComponent::TestHelpers, type: :component
end

Now, we can modify our spec/components/navbar_component_spec.rb file to something like this:

# frozen_string_literal: true

require 'rails_helper'

RSpec.describe NavbarComponent, type: :component do
  describe 'render component' do
    context 'with a logged user' do
      # Here i'm using FactoryBot gem
      let!(:user) { create(:user, name: 'Alex') }

      it 'renders a my account link' do
        render_inline(described_class.new(user: user))
        expect(rendered_component).to include 'My account'
      end
    end

    context 'without a logged user' do
      it 'does not render a my account link' do
        render_inline(described_class.new(user: nil))
        expect(rendered_component).to_not include 'My account'
      end
    end
  end
end

Final considerations

Well, that's awesome! We can render_inline our components and test the content of each one and create different contexts depending on the presence of arguments and much more. This is just what i wanted for my application.

Definitely worth a try! That's it. Thank you for reading.