|
| 1 | +--- |
| 2 | +layout: default |
| 3 | +title: Best practices |
| 4 | +nav_order: 5 |
| 5 | +--- |
| 6 | + |
| 7 | +# Best practices |
| 8 | + |
| 9 | +_A general guide to building component-driven UI in Rails. Consider it to be more opinion than fact._ |
| 10 | + |
| 11 | +## Philosophy |
| 12 | + |
| 13 | +### Why ViewComponent exists |
| 14 | + |
| 15 | +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. |
| 16 | + |
| 17 | +ViewComponent provides a way to isolate common UI patterns for reuse, helping to improve the quality and consistency of Rails applications. |
| 18 | + |
| 19 | +### ViewComponent is to UI what ActiveRecord is to SQL |
| 20 | + |
| 21 | +ViewComponent brings [conceptual compression](https://m.signalvnoise.com/conceptual-compression-means-beginners-dont-need-to-know-sql-hallelujah/) to the practice of building user interfaces. |
| 22 | + |
| 23 | +### ViewComponent exposes existing complexity |
| 24 | + |
| 25 | +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. |
| 26 | + |
| 27 | +This is good! Refactoring to use ViewComponent improves comprehension and provides a foundation for further improvement. |
| 28 | + |
| 29 | +## Organization |
| 30 | + |
| 31 | +### Two types of ViewComponents |
| 32 | + |
| 33 | +ViewComponents typically come in two forms: general-purpose and application-specific. |
| 34 | + |
| 35 | +#### General-purpose ViewComponents |
| 36 | + |
| 37 | +General-purpose ViewComponents implement common UI patterns, such as a button, form, or modal. GitHub open-sources these components as [Primer ViewComponents](https://github.com/primer/view_components). |
| 38 | + |
| 39 | +#### Application-specific ViewComponents |
| 40 | + |
| 41 | +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. |
| 42 | + |
| 43 | +For example, `User::AvatarComponent` accepts a `User` ActiveRecord object and renders a `DesignSystem::AvatarComponent`. |
| 44 | + |
| 45 | +### Extract general-purpose ViewComponents |
| 46 | + |
| 47 | +"Good frameworks are extracted, not invented" - [DHH](https://dhh.dk/arc/000416.html) |
| 48 | + |
| 49 | +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: |
| 50 | + |
| 51 | +1. Single use-case component implemented. |
| 52 | +2. Component adapted for general use in multiple locations in the application. |
| 53 | +3. Component extracted into a general-purpose ViewComponent in `app/lib` or a separate gem. |
| 54 | + |
| 55 | +### Reduce permutations |
| 56 | + |
| 57 | +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. |
| 58 | + |
| 59 | +### Avoid one-offs |
| 60 | + |
| 61 | +Aim to minimize the amount of single-use view code. Every new component introduced adds to application maintenance burden. |
| 62 | + |
| 63 | +## Implementation |
| 64 | + |
| 65 | +### Avoid inheritance |
| 66 | + |
| 67 | +Having one ViewComponent inherit from another leads to confusion, especially when each component has its own template. Instead, [use composition](https://thoughtbot.com/blog/reusable-oo-composition-vs-inheritance) to wrap one component with another. |
| 68 | + |
| 69 | +### When to use a ViewComponent for an entire route |
| 70 | + |
| 71 | +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. |
| 72 | + |
| 73 | +When migrating an entire route to use ViewComponents, work from the bottom up, extracting portions of the page into ViewComponents first. |
| 74 | + |
| 75 | +### Test against rendered content, not instance methods |
| 76 | + |
| 77 | +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: |
| 78 | + |
| 79 | +```ruby |
| 80 | +# good |
| 81 | +render_inline(MyComponent.new) |
| 82 | +assert_text("Hello, World!") |
| 83 | + |
| 84 | +# bad |
| 85 | +assert_equal(MyComponent.new.message, "Hello, World!") |
| 86 | +``` |
| 87 | + |
| 88 | +### Most ViewComponent instance methods can be private |
| 89 | + |
| 90 | +Most ViewComponent instance methods can be private, as they will still be available in the component template: |
| 91 | + |
| 92 | +```ruby |
| 93 | +# good |
| 94 | +class MyComponent < ViewComponent::Base |
| 95 | + private |
| 96 | + |
| 97 | + def method_used_in_template |
| 98 | + end |
| 99 | +end |
| 100 | + |
| 101 | +# bad |
| 102 | +class MyComponent < ViewComponent::Base |
| 103 | + def method_used_in_template |
| 104 | + end |
| 105 | +end |
| 106 | +``` |
| 107 | + |
| 108 | +### Prefer ViewComponents over partials |
| 109 | + |
| 110 | +Use ViewComponents in place of partials. |
| 111 | + |
| 112 | +### Prefer ViewComponents over HTML-generating helpers |
| 113 | + |
| 114 | +Use ViewComponents in place of helpers that return HTML. |
| 115 | + |
| 116 | +### Avoid global state |
| 117 | + |
| 118 | +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: |
| 119 | + |
| 120 | +```ruby |
| 121 | +# good |
| 122 | +class MyComponent < ViewComponent::Base |
| 123 | + def initialize(name:) |
| 124 | + @name = name |
| 125 | + end |
| 126 | +end |
| 127 | + |
| 128 | +# bad |
| 129 | +class MyComponent < ViewComponent::Base |
| 130 | + def initialize |
| 131 | + @name = params[:name] |
| 132 | + end |
| 133 | +end |
| 134 | +``` |
| 135 | + |
| 136 | +Thorough unit testing is a good way to ensure decoupling from global state. |
| 137 | + |
| 138 | +### Avoid inline Ruby in ViewComponent templates |
| 139 | + |
| 140 | +Avoid writing inline Ruby in ViewComponent templates. Try using an instance method on the ViewComponent instead: |
| 141 | + |
| 142 | +```ruby |
| 143 | +# good |
| 144 | +class MyComponent < ViewComponent::Base |
| 145 | + attr_accessor :name |
| 146 | + |
| 147 | + def message |
| 148 | + "Hello, #{name}!" |
| 149 | + end |
| 150 | +end |
| 151 | +``` |
| 152 | + |
| 153 | +```erb |
| 154 | +<%# bad %> |
| 155 | +<% message = "Hello, #{name}" %> |
| 156 | +``` |
| 157 | + |
| 158 | +### Prefer slots over passing markup as an argument |
| 159 | + |
| 160 | +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: |
| 161 | + |
| 162 | +```erb |
| 163 | +# good |
| 164 | +<%= render(MyComponent.new) do |component| %> |
| 165 | + <% component.with_name do %> |
| 166 | + <strong>Hello, world!</strong> |
| 167 | + <% end %> |
| 168 | +<% end %> |
| 169 | +``` |
| 170 | + |
| 171 | +```erb |
| 172 | +# bad |
| 173 | +<%= render MyComponent.new(name: "<strong>Hello, world!</strong>".html_safe) %> |
| 174 | +``` |
0 commit comments