Tools: The Solo Frontend Team: Building a UI System in Pure Ruby

Tools: The Solo Frontend Team: Building a UI System in Pure Ruby

Source: Dev.to

The "Partial" Problem ## What is a ViewComponent? ## The Old Way (Partial) ## The New Way (Component) ## Why this changes everything for Solo Devs ## 1. The End of Silent Failures ## 2. Logic Belongs in Ruby, not HTML ## 3. Testing in Isolation (The Superpower) ## 4. Lookbook / Previews ## Summary We love Rails. We love ERB. But let's be honest: app/views is usually the messiest part of any Rails codebase. You start simple. Then you extract a partial. Then you need to pass a local variable. Then you need to add logic: Only show the footer if the user is an admin. Suddenly, your HTML file is full of if/else statements, Ruby logic, and loose variables. If you misspell user as use, Rails won't complain until the page renders and crashes. For years, the industry told us the solution was React. "Stop writing ERB! Build an API and write the frontend in JavaScript!" But for a solo developer or a small team, that is a massive overhead. Enter ViewComponent. Created by GitHub (and used to render the UI you are looking at right now if you visit GitHub.com), ViewComponent brings the "Component Architecture" of React into the Ruby world. Instead of a loose template file, a ViewComponent is a Ruby Object. _user_badge.html.erb: Problem: Testing this in isolation is impossible. You have to load a whole page that contains it. app/components/user_badge_component.rb: app/components/user_badge_component.html.erb: Now, in your views, you render it like an object: In the example above, initialize requires a keyword argument user:. If you try to render that component without passing a user, Ruby raises an error immediately. It enforces a strict interface for your UI. No more guessing which locals a partial needs. Notice how the background_color logic moved into the Ruby class? This keeps your template file clean. You can write complex methods, use guard clauses, and handle edge cases in standard Ruby code, leaving your HTML to just be... HTML. This is the game changer. Usually, to test a UI element in Rails, you write a System Test (Capybara). It boots the browser, loads the database, visits the page, and clicks around. It is slow. With ViewComponents, you write Unit Tests for your UI. These tests run in milliseconds. You can test every edge case of your UI without ever launching a browser. ViewComponents allow you to use tools like Lookbook. It creates a "Storybook" for your Rails app. You can browse a gallery of all your buttons, modals, and cards in a dashboard, change their states, and see how they look. It makes you feel like you have a dedicated Frontend Team managing a design system, even if it's just you. ViewComponents are the "Goldilocks" solution. They give you the organization and testability of React, with the speed and simplicity of Ruby on Rails. Have you started refactoring your partials into components yet? Let me know! 👇 Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK: <%= render partial: "shared/card", locals: { title: "Hello", show_footer: true, user: @user } %> Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: <%= render partial: "shared/card", locals: { title: "Hello", show_footer: true, user: @user } %> CODE_BLOCK: <%= render partial: "shared/card", locals: { title: "Hello", show_footer: true, user: @user } %> CODE_BLOCK: <div class="badge <%= user.admin? ? 'bg-red-500' : 'bg-blue-500' %>"> <%= user.name %> </div> Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: <div class="badge <%= user.admin? ? 'bg-red-500' : 'bg-blue-500' %>"> <%= user.name %> </div> CODE_BLOCK: <div class="badge <%= user.admin? ? 'bg-red-500' : 'bg-blue-500' %>"> <%= user.name %> </div> CODE_BLOCK: rails g component UserBadge user Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: rails g component UserBadge user CODE_BLOCK: rails g component UserBadge user CODE_BLOCK: class UserBadgeComponent < ViewComponent::Base def initialize(user:) @user = user end def background_color @user.admin? ? "bg-red-500" : "bg-blue-500" end end Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: class UserBadgeComponent < ViewComponent::Base def initialize(user:) @user = user end def background_color @user.admin? ? "bg-red-500" : "bg-blue-500" end end CODE_BLOCK: class UserBadgeComponent < ViewComponent::Base def initialize(user:) @user = user end def background_color @user.admin? ? "bg-red-500" : "bg-blue-500" end end CODE_BLOCK: <div class="badge <%= background_color %>"> <%= @user.name %> </div> Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: <div class="badge <%= background_color %>"> <%= @user.name %> </div> CODE_BLOCK: <div class="badge <%= background_color %>"> <%= @user.name %> </div> CODE_BLOCK: <%= render UserBadgeComponent.new(user: @current_user) %> Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: <%= render UserBadgeComponent.new(user: @current_user) %> CODE_BLOCK: <%= render UserBadgeComponent.new(user: @current_user) %> COMMAND_BLOCK: # spec/components/user_badge_component_spec.rb require "rails_helper" RSpec.describe UserBadgeComponent, type: :component do it "renders red for admins" do admin = User.new(role: :admin, name: "Boss") render_inline(described_class.new(user: admin)) expect(page).to have_css ".bg-red-500" expect(page).to have_text "Boss" end end Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: # spec/components/user_badge_component_spec.rb require "rails_helper" RSpec.describe UserBadgeComponent, type: :component do it "renders red for admins" do admin = User.new(role: :admin, name: "Boss") render_inline(described_class.new(user: admin)) expect(page).to have_css ".bg-red-500" expect(page).to have_text "Boss" end end COMMAND_BLOCK: # spec/components/user_badge_component_spec.rb require "rails_helper" RSpec.describe UserBadgeComponent, type: :component do it "renders red for admins" do admin = User.new(role: :admin, name: "Boss") render_inline(described_class.new(user: admin)) expect(page).to have_css ".bg-red-500" expect(page).to have_text "Boss" end end - Too Cold: Messy, untestable ERB partials. - Too Hot: A complex React/Next.js frontend separated from your backend. - Just Right: ViewComponents.