Best practices
A general guide to building component-driven UI in Rails. Consider it to be more opinion than fact.
Philosophy
Why ViewComponent exists
ViewComponent was created to help manage the growing complexity of the GitHub.com view layer, which accumulated thousands of templates over the years, almost entirely through copy-pasting. A lack of abstraction made it challenging to make sweeping design, accessibility, and behavior improvements.
ViewComponent provides a way to isolate common UI patterns for reuse, helping to improve the quality and consistency of Rails applications.
ViewComponent is to UI what ActiveRecord is to SQL
ViewComponent brings conceptual compression to the practice of building user interfaces.
ViewComponent exposes existing complexity
Converting an existing view/partial to a ViewComponent often exposes existing complexity. For example, a ViewComponent may need numerous arguments to be rendered, revealing the number of dependencies in the existing view code.
This is good! Refactoring to use ViewComponent improves comprehension and provides a foundation for further improvement.
Organization
Two types of ViewComponents
ViewComponents typically come in two forms: general-purpose and application-specific.
General-purpose ViewComponents
General-purpose ViewComponents implement common UI patterns, such as a button, form, or modal. GitHub open-sources these components as Primer ViewComponents.
Application-specific ViewComponents
Application-specific ViewComponents translate a domain object (such as an ActiveRecord
model or an API response modeled as a Plain Old Ruby Object) into one or more general-purpose components.
For example, User::AvatarComponent
accepts a User
ActiveRecord object and renders a DesignSystem::AvatarComponent
.
Extract general-purpose ViewComponents
“Good frameworks are extracted, not invented” - DHH
Just as ViewComponent itself was extracted from GitHub.com, general-purpose components are best extracted once they’ve proven helpful across more than one area:
- Single use-case component implemented.
- Component adapted for general use in multiple locations in the application.
- Component extracted into a general-purpose ViewComponent in
app/lib
or a separate gem.
Reduce permutations
When building ViewComponents, look for opportunities to consolidate similar patterns into a single implementation. Consider following standard DRY practices, abstracting once there are three or more similar instances.
Avoid one-offs
Aim to minimize the amount of single-use view code. Every new component introduced adds to application maintenance burden.
Implementation
Avoid inheritance
Having one ViewComponent inherit from another leads to confusion, especially when each component has its own template. Instead, use composition to wrap one component with another.
When to use a ViewComponent for an entire route
ViewComponents have less value in single-use cases like replacing a show
view. However, it can make sense to render an entire route with a ViewComponent when unit testing is valuable, such as for views with many permutations from a state machine.
When migrating an entire route to use ViewComponents, work from the bottom up, extracting portions of the page into ViewComponents first.
Test against rendered content, not instance methods
ViewComponent tests should use render_inline
and assert against the rendered output. While it can be useful to test specific component instance methods directly, it’s more valuable to write assertions against what’s shown to the end user:
# good
render_inline(MyComponent.new)
assert_text("Hello, World!")
# bad
assert_equal(MyComponent.new.message, "Hello, World!")
Most ViewComponent instance methods can be private
Most ViewComponent instance methods can be private, as they will still be available in the component template:
# good
class MyComponent < ViewComponent::Base
private
def method_used_in_template
end
end
# bad
class MyComponent < ViewComponent::Base
def method_used_in_template
end
end
Prefer ViewComponents over partials
Use ViewComponents in place of partials.
Prefer ViewComponents over HTML-generating helpers
Use ViewComponents in place of helpers that return HTML.
Avoid global state
The more a ViewComponent is dependent on global state (such as request parameters or the current URL), the less likely it’s to be reusable. Avoid implicit coupling to global state, instead passing it into the component explicitly:
# good
class MyComponent < ViewComponent::Base
def initialize(name:)
@name = name
end
end
# bad
class MyComponent < ViewComponent::Base
def initialize
@name = params[:name]
end
end
Thorough unit testing is a good way to ensure decoupling from global state.
Avoid inline Ruby in ViewComponent templates
Avoid writing inline Ruby in ViewComponent templates. Try using an instance method on the ViewComponent instead:
# good
class MyComponent < ViewComponent::Base
attr_accessor :name
def message
"Hello, #{name}!"
end
end
<%# bad %>
<% message = "Hello, #{name}" %>
Prefer slots over passing markup as an argument
Prefer using slots for providing markup to components. Passing markup as an argument bypasses the HTML sanitization provided by Rails, creating the potential for security issues:
# good
<%= render(MyComponent.new) do |component| %>
<% component.with_name do %>
<strong>Hello, world!</strong>
<% end %>
<% end %>
# bad
<%= render MyComponent.new(name: "<strong>Hello, world!</strong>".html_safe) %>