| layout | title | nav_order |
|---|---|---|
default |
Best practices |
5 |
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 brings conceptual compression to the practice of building user interfaces.
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.
ViewComponents typically come in two forms: general-purpose and application-specific.
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 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.
"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/libor a separate gem.
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.
Aim to minimize the amount of single-use view code. Every new component introduced adds to application maintenance burden.
While it means class names are longer and perhaps less readable, including the -Component suffix in component names makes it clear that the class is a component, following Rails convention of using suffixes for all non-model objects.
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.
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.
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, 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
endUse ViewComponents in place of partials.
Use ViewComponents in place of helpers that return HTML.
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
endThorough unit testing is a good way to ensure decoupling from global state.
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 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) %>