ViewComponent (Advanced)
Best Practices
DO
- Use composition instead of inheritance
# GOOD - Composition
class PanelComponent < ViewComponent::Base
renders_one :card, CardComponent
end
# BAD - Inheritance
class PanelComponent < CardComponent
end
- Extract components after proving pattern across multiple uses
# Good frameworks are extracted, not invented
# Develop single-use components first, extract when pattern repeats 3+ times
- Pass global state explicitly as arguments
# GOOD
render UserCardComponent.new(user: current_user, signed_in: user_signed_in?)
# BAD - Accessing global state
class UserCardComponent < ViewComponent::Base
def call
if user_signed_in? # Don't access global state
...
end
end
end
- Use instance methods instead of inline Ruby in templates
# GOOD
class ButtonComponent < ViewComponent::Base
private
def button_classes
["btn", "btn-#{type}", size_class].compact.join(" ")
end
end
<button class="<%= button_classes %>">
- Prefer slots for providing markup to components
# GOOD - Using slots
card.with_header do
content_tag :h3, "Title"
end
# BAD - Passing HTML as argument
card.header = "<h3>Title</h3>".html_safe
- Test against rendered content
# GOOD
def test_renders_button
render_inline ButtonComponent.new(type: :primary)
assert_selector "button.btn-primary"
end
# BAD - Testing only instance methods
def test_button_classes
component = ButtonComponent.new(type: :primary)
assert_equal "btn btn-primary", component.send(:button_classes)
end
- Make most instance methods private
class ButtonComponent < ViewComponent::Base
def initialize(type:)
@type = type
end
# Public interface is minimal
private
attr_reader :type
# Helper methods are private but accessible in templates
def button_classes
"btn btn-#{type}"
end
end
- Replace partials and HTML-generating helpers
# GOOD - ViewComponent
render ButtonComponent.new(type: :primary, url: user_path(@user))
# OLD - Partial
render "shared/button", type: :primary, url: user_path(@user)
# OLD - Helper
button_tag type: :primary, url: user_path(@user)
DON'T
- Don't use component inheritance with separate templates
# BAD - Confusing inheritance
class PanelComponent < CardComponent
# Has its own template - which one renders?
end
- Don't write inline Ruby in templates
<%# BAD %>
<button class="btn <%= type == :primary ? 'btn-primary' : 'btn-secondary' %>">
<%# GOOD - Use instance method %>
<button class="<%= button_classes %>">
- Don't pass HTML-safe markup as arguments
# BAD - Security risk
render CardComponent.new(title: "<h3>#{user_input}</h3>".html_safe)
# GOOD - Use slots
render CardComponent.new do |card|
card.with_title { content_tag :h3, user_input }
end
- Don't rely on global state
# BAD
class UserComponent < ViewComponent::Base
def call
current_user # Accessing global state
params[:id] # Accessing request params
end
end
# GOOD - Explicit dependencies
class UserComponent < ViewComponent::Base
def initialize(user:, id:)
@user = user
@id = id
end
end
Source: ViewComponent Best Practices
Component Organization
Two Component Types
General-purpose components - Common UI patterns
- Examples: buttons, forms, modals, alerts
- Like Primer ViewComponents
- Highly reusable across applications
Application-specific components - Domain-driven
- Examples: UserCard, ProductListing, InvoiceHeader
- Convert domain objects into general-purpose components
- Encapsulate business logic presentation
Naming Conventions
# Use -Component suffix (Rails conventions)
ButtonComponent
UserCardComponent
NavigationComponent
Extraction Strategy
1. Develop single-use components first
2. Extract to reusable component once pattern appears 3+ times
3. Consolidate similar patterns (DRY)
4. Minimize single-use view code
Source: ViewComponent Best Practices
Lifecycle Methods
class Component < ViewComponent::Base
def initialize(*args)
# Called when component is instantiated
super
end
def before_render
# Called before rendering
# Access to slots here
@computed_value = expensive_calculation if header?
end
def call
# Optional: custom render logic
# By default renders the template
content_tag :div, class: "wrapper" do
super
end
end
end
Source: ViewComponent Lifecycle Guide
Advanced Patterns
Collections
class UserComponent < ViewComponent::Base
def initialize(user:)
@user = user
end
# Enable collection rendering
with_collection_parameter :user
end
Usage:
<%# Renders UserComponent for each user %>
<%= render UserComponent.with_collection(@users) %>
<%# With counter %>
<%= render UserComponent.with_collection(@users, :user_counter) %>
Conditional Rendering
class Component < ViewComponent::Base
def render?
# Return false to skip rendering entirely
user.present? && user.active?
end
end
Helpers
class Component < ViewComponent::Base
# Access Rails helpers
def formatted_date
helpers.time_ago_in_words(created_at)
end
# Or delegate
delegate :link_to, :content_tag, to: :helpers
end
Common Patterns
Form Component
class FormComponent < ViewComponent::Base
renders_one :submit_button, ButtonComponent
def initialize(url:, method: :post, **options)
@url = url
@method = method
@options = options
end
private
attr_reader :url, :method, :options
end
<%= form_with url: url, method: method, **options do |f| %>
<%= content %>
<% if submit_button? %>
<%= submit_button %>
<% else %>
<%= f.submit "Submit", class: "btn btn-primary" %>
<% end %>
<% end %>
Modal Component
class ModalComponent < ViewComponent::Base
renders_one :title
renders_one :body
renders_many :actions, ActionComponent
def initialize(id:, size: :medium)
@id = id
@size = size
end
private
attr_reader :id, :size
end
Table Component
class TableComponent < ViewComponent::Base
renders_many :columns, ColumnComponent
renders_many :rows, RowComponent
def initialize(data:, **options)
@data = data
@options = options
end
end
Summary
- ViewComponent = Reusable, testable, encapsulated view components
- 100x faster tests than controller tests
- Slots =
renders_one(single) andrenders_many(multiple) - Testing = Use
render_inlinewith Capybara matchers - Previews = Visualize components at
/rails/view_components - Best Practices = Composition over inheritance, explicit dependencies, slots over HTML args
- Organization = General-purpose vs application-specific components
References and Sources
This guide is based on official ViewComponent documentation:
- ViewComponent Official Documentation
- ViewComponent GitHub Repository
- ViewComponent Guide
- ViewComponent Slots
- ViewComponent Testing
- ViewComponent Previews
- ViewComponent Best Practices
Last updated: December 2025
Converted and distributed by TomeVault — claim your Tome and manage your conversions.