Facundo Espinosa

August 11, 2022

Reduce ViewComponents complexity using helpers

Yesterday I figured out how to avoid a pattern that we’ve been doing in a project for some time which is sending the current_user in components through parameters to validate things based on who is watching.

Example:

```erb
<%= render ProductsComponent.new(current_user: current_user, products: @products) %>

```ruby
class ProductsComponent < ViewComponent::Base
  attr_reader :current_user, :products

  def initialize(current_user:, products:)
    @current_user = current_user
    @products = products
  end
end

```erb
<table>
  <tr>
    <th>Title</th>
    <th>Description</th>
    <th>Actions</th>
  </tr>
  <% products.each do |product| %>
    <tr>
      <td><%= product.title %></td>
      <td><%= product.description %></td>
      <td>
        <% if ProductsPolicy.new(current_user, product).destroy? %>
          <i class="fa fa-times"></i>
        <% end %>
      <td>
    </tr>
  <% end %>
</table>
  • Note that we're using pundit. That's why we need to use the current user in the component, to decide based on the policies, something that we want to hide if you don't have permission to see it.

But pundit and devise provide helpers that make your life easier, like policy and current_user. But how do we use them in a component? Components need you to include those helpers modules to be available. Otherwise, components can't see those helpers. So let's use them and refactor the previous code:

```erb
<%= render ProductsComponent.new(products: products) %>

```ruby
class ProductsComponent < ViewComponent::Base
  include Pundit::Authorization
  include Devise::Controllers::Helpers

  attr_reader :products

  def initialize(products:)
    @products = products
  end
end

```erb
<table>
  <tr>
    <th>Title</th>
    <th>Description</th>
    <th>Actions</th>
  </tr>
  <% products.each do |product| %>
    <tr>
      <td><%= product.title %></td>
      <td><%= product.description %></td>
      <td>
        <% if policy(product).destroy? %>
          <i class="fa fa-times"></i>
        <% end %>
      <td>
    </tr>
  <% end %>
</table>

BENEFITS:
  • Use pundit helper policy to make the code easier to read and use the conventions over configuration that pundit provides (Note that we need to include Pundit::Authorization  on the component)
  • Avoid sending current_user as a parameter of the component when is not directly related to it. In the code example, the important resource is the product list, not the current_user. Multiple times we're sending the current_user just to validate permissions in the component adding unnecessary complexity to the component initialization. How can we take advantage of devise here? Including the Devise::Controllers::Helpers  so we can access to the current_user  helper as any other view.

HOW CAN WE ADD SPECS BASED ON PERMISSIONS FOR THESE SCENARIOS?

Well, the important thing to understand is that the kind of user is not important in the context of the components. The only thing that matters is what the policy method return and how that affects the UI rendered by the component. That’s what we want to validate so we can do something like:

```ruby
describe 'destroy icon' do
  context 'when the user can destroy a product' do
    it 'renders destroy icon' do
      component = described_class.new(products: [build(:product)])
      allow(component).to receive(:policy).and_return(instance_double(ProductPolicy, destroy?: true)) # note that here is where we decide the behavior

      render_inline component

      expect(rendered_content).to have_selector('i.fa-times')
    end
  end

  context 'when the user can NOT destroy a product' do
    it 'does NOT render destroy icon' do
      component = described_class.new(products: [build(:product)])
      allow(component).to receive(:policy).and_return(instance_double(ProductPolicy, destroy?: false)) # note that here is where we decide the behavior

      render_inline component

      expect(rendered_content).not_to have_selector('i.fa-times')
    end
  end
end

CONCLUSIONS:

  • Remember that we can include helpers in the components. 
    • Sometimes gems request us to include some helper modules in the ApplicationController so that we can access them in controllers and views, but now we have a new layer that doesn't have access to them with just that include.
  • Try to identify those scenarios where you're sending parameters to the component not directly related to the main resource you want to render and think if it's possible to reduce complexity using some existing helpers.