DEV Community

Stephann
Stephann

Posted on

Os benefícios de componentizar as views do Rails

Introdução

Uma coisa que contribui bastante para um código bagunçado e confuso no Ruby on Rails, principalmente em aplicações que crescem e passam pelas mãos de diversas pessoas, é a forma como o Rails lida com o seu front-end, suas views, helpers e partials, e todo o processo de escrever código para gerar o HTML. E nesse artigo eu proponho uma solução para esse problema utilizando as views como objetos.

Qual o problema com o front-end do Rails?

Antes de tudo, quero iniciar deixando claro que eu adoro escrever aplicações full stack com o Ruby on Rails. É muito mais fácil resolver certos problemas utilizando o Rails completo do que criar uma aplicação Rails API e construir um SPA (Single Page Application) com um framework javascript. Coisas como autenticação, autorização, SEO, deploy, e outras coisas que devo ter esquecido de mencionar, são horríveis de implementar em aplicações SPA. Mas por alguns motivos, houve um movimento da comunidade na direção de utilizar o Rails apenas para construir as APIs REST mesmo quando isso não era um requisito do produto, e talvez um dos motivos que auxiliou esse movimento tenha sido a facilidade de fazer bagunça na construção do HTML com o Ruby on Rails.

Pra exemplificar, vou utilizar esse código simples pra expor alguns problemas, é um código em uma partial app/views/layouts/_menu.html.erb:

<%= badge(color: :red, text: @notifications.count) %>
Enter fullscreen mode Exit fullscreen mode

Se alguém quisesse saber de onde veio esse método bagde, a pessoa teria que buscar nos app/helpers/**/*.rb, que inclusive poderia estar definido mais de uma vez em múltiplos helpers sem ninguém notar, ou poderia até estar definido em um controller, com o método helper. Aqui fica evidente um problema, que os helpers são globais por padrão, então qualquer método definido na pasta helpers estará disponível para ser utilizado em qualquer lugar da view. Pra resolver isso, há a configuração config.action_controller.include_all_helpers = false, que vai adicionar apenas no controller apenas o helper correspondente ao recurso, mas, apesar de ajudar a resolver o problema mencionado, não resolve a tendência dos helpers ficarem inchados com o passar do tempo para comportar os métodos das views e partials daquele recurso.

Outro problema com o código: De onde esse @notifications veio? Você procura no controller da tela em questão, por exemplo ArticlesController, olha nas ações, nos métodos privados, nos callbacks *_action, e não encontra, depois procura no ApplicationController, também não encontra, mas descobre que foi incluído o concern app/controllers/concern/notifications_concern.rb que tinha o seguinte código:

module NotificationsConcern
  included do
    after_action do
      @notifications = Notification.where(
        user: current_user,
        read: false
      )
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Isso expõe o problema, há muitos vetores de entrada de dados nas views. Um callback que instancia uma variável, foi definido num concern, que foi incluído no ApplicationController, e que uma partial renderizada na view terá acesso direto a essa variável, sem nenhum contrato explícito. Fica mentalmente confuso de entender o código, debuggar, saber do que uma tela precisa pra ser construída, entender o fluxo de dados. Uma solução pra isso seria: Fazer um acordo com o time para que esse tipo de coisa não seja feita, mas não seria tão fácil garantir que a aplicação nunca tenha um código assim.

E não vou nem me aprofundar na dificuldade de testar código relacionado ao front-end. Mas há como amenizar todos esses problemas mencionados relacionados às views, partials e helpers. Eu penso que tratar as páginas como objetos é um grande começo.

Tratando as páginas como objetos

Até um tempo atrás, as partials do Rails eram a única forma de componentizar pedaços da sua tela, os helpers também ajudavam nesse ponto, mas como mencionei na seção anterior, esses recursos têm um potencial de conter código insustentável. Recentemente as bibliotecas de componentização têm ganhado seu espaço na comunidade, com a ViewComponent sendo a principal biblioteca utilizada para isso, mas há várias alternativas, uma delas é o Phlex, que já testei e recomendo.

Mas enquanto a maioria dessas bibliotecas focam em componentes (pedaços da tela) e na reutilização desses componentes como um dos grandes benefícios, não há um incentivo ao uso da mesma estratégia para a criação de páginas. Inclusive ao expor que estou utilizando componentes para a criação de páginas, recebi alguns comentários assim: "Utilizar componentes para páginas seria a última coisa que eu faria, pois páginas não são reutilizáveis e não faria sentido componentizá-las", sim isso foi um comentário real no Twitter, não são vozes da minha cabeça, eu juro. Mas será que comentários assim fazem sentido? Talvez, pode ser que sim, realmente as páginas não precisam ser reutilizáveis, mas aplicar a "componetização" nelas traz todos os demais benefícios dessa estratégia, e pra reforçar isso, podemos ler a seguir, de forma resumida, o que o site do ViewComponent lista como vantagens na sua utilização:

Por que utilizar o ViewComponent?

Responsabilidade única: As lógicas relacionadas às views geralmente estão espalhadas entre modelos, controllers e helpers, diluindo suas reponsabilidades. Com o ViewComponent essa lógica fica encapsulada em suas próprias classes, facilitando o entendimento.

Testes: Os componentes são testados individualmente. Dessa forma os testes de integração servem pra testar a aplicação de ponta a ponta, enquanto as variações do conteúdo renderizado são testadas com testes unitários.

Fluxos de dados: Os templates do rails (views/partials) têm uma interface implítica, tornando difícil de enxergar as dependências. Com ViewComponent, os componentes tem um construtor que identifica fácilmente o que é necessário para a sua renderização.

Qualidade de código: Geralmente os templates (views/partials) falham nos padrões de qualidade básico, pois têm métodos longos e condicionais aninhadas em múltiplos níveis. ViewComponents são objetos Ruby, facilitando e garantindo a escrita de código com qualidade.

Pode parecer uma grande loucura o que vou escrever agora, mas eu quero essas vantagens pra minhas páginas também. A seguir explico como estou fazendo isso com o ViewComponent nas aplicações que escrevo:

Utilizando o ViewComponent para criar páginas

Como falei anteriormente, as bibliotecas de componentização não focam muito na criação de páginas, mas nada impede que você utilize um componente como uma página. Quando utilizo a ViewComponent eu prefiro criar uma pasta dedicada para as páginas, e deixar a pasta padrão app/components que a gem sugere para os componentes em si. Minha abordagem é a seguinte:

  • Crio a pasta app/pages
  • Modularizo as classes pelo seu recurso, ou seja, se é uma página de listagem de contatos, ela ficará dentro do módulo Contacts
  • Adiciono o sufixo Page às classes pra ficar claro o que aquela classe representa

Então uma listagem de contatos ficaria mais ou menos assim:

# app/pages/contacts/index_page.rb

module Contacts
  class IndexPage < ViewComponent::Base
    attr_reader :contacts

    def initialize(contacts:)
      @contacts = contacts
    end

    private

    def emergency_badge(contact)
      if contact.emergency?
        render BagdeComponent.new(color: :red) do
          "Emergency"
        end
      end
    end
  end
end
Enter fullscreen mode Exit fullscreen mode
<%# app/pages/contacts/index_page.html.erb %>

<h1>Contacts</h1>
<% contacts.each do |contact| %>
  ...
  <%= emergency_badge(contact)
<% end %>
Enter fullscreen mode Exit fullscreen mode
# app/controllers/contacts_controllers.rb

class ContactsController < ApplicationController
  def index
    contacts = Contacts.all

    render Contacts::IndexPage.new(contacts: contacts)
  end
end
Enter fullscreen mode Exit fullscreen mode
# spec/pages/contacts/index_page_spec.rb

RSpec.describe Contacts::IndexPage do
  ...

  context "when some contact is an emergency contact" do
    it "renders emergency bagde" do
      contact = Contact.new(
        name: "John", 
        email: "x@y.z", 
        emergency: true
      )

      render_inline(described_class.new(contacts: [contact]))

      expect(page).to have_css(
        "span.bg-red-200", text: "Emergency"
      )
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Com esse código podemos ver os problemas mencionados nas seções anteriores sendo resolvidos:

  • Fluxo de dados: No controller, fica claro o que é necessário para renderizar a página, nenhuma variável de instância, ex.: @something, irá "vazar" e ser acessada pelo objeto da página.
  • Qualidade de código: A lógica de renderização é extraída para a classe da página, tornando o template mais simples de ler e manter em comparação com os if, elses e um monte de código ruby dentro dos .html.erb.
  • Responsabilidade única e a bagunça dos helpers é evitada: A lógica de renderizar a badge de contato de emergência está bem próxima de onde está sendo utilizada, ou seja, no próprio objeto da página, e não perdida em algum helper, método no controller, modelo e etc.
  • Testes: Conseguimos testar as variações da página com testes unitários, deixando os testes de integração mais simples e focados no que devem fazer.

Claro, você ainda precisará fazer algumas coisinhas pra melhorar a developer experience, como criar um ApplicationPage pra compartilhar comportamentos gerais, criar um gerador rails g page contacts/show pra facilitar a criação de páginas, ajustar a questão das traduções, já que o t(".my_key") não vai funcionar tão bem por padrão, quando quiser um comportamento compartilhado entre algumas poucas telas/componentes vai ter que extrair pra um módulo e incluir nas respectivas classes, mas nada de outro mundo que não valha a pena a adoção dessa estratégia.

Conclusão

Tenho utilizado essa estratégia nas últimas aplicações que trabalhei e tem funcionado bem, também ando testando a gem Phlex com outras formas de organização e também tem funcionado, e no momento não tenho sentido falta de nada do jeito padrão do Rails trabalhar com seus templates, pelo contrário, tem ajudado a manter uma organização e padronização que eu não tinha anteriormente.

Inclusive acredito que isso deveria ser uma evolução no Rails, já vir com uma solução dessa disponível embutida no próprio framework, com documentação, com scaffold sendo gerado assim, pois pode até parecer mais verboso no princípio, mas é uma troca que acho que vale muito a pena. As mágicas do Rails são incríveis quando você está vindo de um outro framework e te encantam, mas conforme você enxerga os problemas que algumas mágicas trazem, você passa a querer as coisas da forma mais explícita o possível, mas a gente só aprende isso depois de sofrer um pouco.

Mas o que eu trouxe aqui não é uma ideia nova, inclusive inspirado no Hanami e no Lucky foi que resolvi fazer algo parecido no Rails. O Hanami tem suas views como objetos e o Lucky (esse é um framework web utilizando a linguagem Crystal) também adota a mesma estratégia, e alguns frameworks javascript, como é o caso do Nuxt têm coisas parecidas. Quem sabe numa versão 8.0 o Rails também entra nesse time.

Top comments (2)

Collapse
 
evertonlopesc profile image
Everton Lopes

Ótimo post, curti muito!

Obrigado por compartilhar.

Collapse
 
juneira profile image
Marcelo Junior

Já havia lido a respeito desse framework e não vejo porque o Rails não adota esse modelo.

Muito bom o post, gostei bastante do exemplo que usou!