Recently, we began using ViewComponents in our project to help us build the redesigned admin section of our web. We want to share our decision process that made us try this framework in the first place and how well it went. TL;DR? We love it! ❤
Being a long-term product-oriented project, every now and then we find ourselves rewriting some, even fully working, pages from scratch: to refresh the design, to speed up the page load or to apply new coding standards and get rid of the old ones. Now, the time has come to our old and rusty admin section.
We have lots of – fairly standard – admin pages: with index tables, details and edit forms. We estimated that about 80-90% of our admin pages could look and behave in a more or less unified way, the rest being special pages that must be carefully optimized for the most essential needs our administrators have when doing their job. Although a large part of our admin was already written in a reusable way, we wanted to revamp the look and feel, bringing in new standards including styling via Tailwind CSS or higher interactivity with the Hotwire stack.
We knew from the start that rebuilding admin section was a perfect case for developing and applying components in the view layer. By ”a component“ we mean a piece of reusable code, isolated and encapsulated from others that in the end gets rendered as a visually and functionally distinct part of a webpage. A component should easily provide a default look but still allow flexibility if needed (some support for more visual variants and / or behaviors).
To build a user interface made of reusable components, we first had to choose a template or a design library that would promise a consistent look for all the various page sections that we needed. We assessed many such Tailwind-ready templates focusing on admin interfaces but eventually decided to invest in Tailwind UI. And we never regretted since!
Tailwind UI is a set of wonderfully crafted visual components made with an eye for detail and with responsiveness in mind, perfectly suitable for the latest Tailwind CSS versions. It’s a showcase of what the Tailwind design gurus think is nice and functional and even though we didn’t use the advanced features such as Vue/React templates, we learned a lot from the code samples. What we particularly liked about Tailwind UI is that there was often more than one alternative laid out for a visual feature giving us more options to choose and be inspired from.
What about a full-fledged Rails admin gem?
We briefly considered migrating to a full-grown Rails admin interface, such as ActiveAdmin, RailsAdmin, Administrate or Avo. We especially liked Avo which is built on a very modern stack similar to ours (Tailwind + Hotwire + ViewComponents). In the end, we didn’t go this route as we found some of the options a bit too restrictive (even though Avo is very flexible) and we did not feel like trying to amend it to our needs. For example, Avo renders forms in a 1-field-per-row layout while we wanted something more similar to the Tailwind UI Stacked form layout. Nevertheless, we found a great deal of inspiration in the Avo code and its design principles.
After we settled down on the design, we pondered about a suitable way to add the front-end components to our code base. What options do Rails actually give us in the first place? We considered partial templates, Rails helpers and then we moved on to other possible solutions. Below is a brief summary of our thought process at that time.
Partial template (or ”partial“) is a standard Rails way to extract a piece of template code to its own file. The partial then can be called (rendered) from other templates, helpers or controllers.
Partials, like all templates, are HTML-centric – they are the strongest for embedding various HTML tags in a structure which is then rendered on a web page. However, they are not that great if you need to add some non-trivial logic – a template with more than a few control statements can quickly become messy.
In our opinion, the biggest issue, from a ”component“ point of view, is their lack of isolation. A partial template freely recognizes all instance variables (
@variable) and this makes the template tightly coupled with the controller layer where these variables are typically defined. Suppose we’d use a partial template on five different pages: we would have to define the same instance variable(s) in all five actions of the corresponding controllers. Even worse, instance variables default to
nil so forgetting to properly set a variable does not raise an exception, instead it can just lead to an unexpected blank output.
Sure, we could pass the data as local variables instead (and we heartily encourage such a more explicit style of passing data to partials) but this convention would have to be guarded and enforced all over the team. We actually run a custom Overcommit hook to encourage this explicit style in our project. Still, the
locals hash is just… a
Hash and makes the partials API only moderately flexible for us, especially since we settled down on using keyword argument APIs wherever possible.
Helpers are another ”Railsy“ option to componentize things in the view layer. They are simple ruby functions, living inside a
As opposed to templates, Rails helpers are ruby-centric. Being just normal ruby functions, they support any API style for passing parameters that is supported by your ruby version, including keyword arguments.
But the easier it is to call and embed ruby code in a helper, the harder it tends to be to combine more HTML tags there into a renderable structure. Simple tags are fine and there are a multitude of pre-defined ActionView helpers available, such as
link_to that reduce repetition. But building a more complex HTML structure inside a helper can become a painful experience. Sooner than later one finds that he or she needs to
concat things, perhaps even
capture things and all the time they must be aware of the possible security implications of building such HTML structure and learn about
safe_concat and similar stuff while templates partly mitigate this problem by regarding raw HTML tags as
html_safe by default.
While helpers can deal with ruby code logic more elegantly than templates, they are nowhere near ruby classes or objects in terms of flexibility. Helpers are – similarly to templates – not isolated from instance variables but it is perhaps a bit more straightforward to obey a convention of using only function parameters to pass data to them. But helpers are also global: each helper is available in the whole view layer, which means that their names must be unique and that categorizing them into multiple files (modules) makes less sense as it brings no real encapsulation. Add to it the fact that Rails itself defines dozens of helpers so the helpers namespace can become quite cluttered and name collisions with hard-to-debug surprises may occur.
It is easy to unit-test Rails helpers but only as plain ruby functions, for example it is not possible to use Capybara on the generated output by default.
We’ve seen that partial templates and helpers have each their own strengths and weaknesses in terms of building components. So why not let each of them focus on what they can do best? Indeed, partials and helpers are meant to cooperate: one can easily call helpers from templates as well as render partials from helper functions.
While we were sure that we could get pretty far using a combination of helpers and templates, by the time we were assessing this option, we already knew we wanted to look elsewhere. To us, the two worlds are too distinct for building components, the two types of code are located too far from each other without an obvious interconnection between them. Partials and helpers may play together well but they still don’t make a clear unit.
So what about the world outside Rails defaults? There are quite a few independent projects trying to help build components in the Rails view layer, among the more famous being Draper (utilizing the decorators pattern) or Cells (full-featured components in views). In the end, we decided to take a deeper look into a relatively new one – the ViewComponent framework.
The ViewComponent framework has originally been developed and used extensively at GitHub. It provides a set of conventions to build components in the view layer that should make them well encapsulated, reusable, flexible and testable. Below are our comments to features that we particularly liked about View Components:
View Components have an explicit home in the code base. By ”having a home“ we not only mean that view components reside under the
app/componentsfolder but also the fact that the code for the component behavior as well as its template live next to each other, in the same place in the code base. The components code can be categorized into folders by their meaning or function rather than technology.
The component ruby file supports logic of any complexity. A component is just a ruby class so we can leverage all features of object-oriented programming in them such as private methods, composition, inheritance and just about anything else, if needed. The template file, on the other hand, can stay virtually logic-less.
View components are truly encapsulated. All configuration and data for the component must be explicitly passed in via the
initializemethod arguments or blocks. Even Rails helpers are not automatically recognized and must be explicitly included or accessed through a
The rendering of components is flexible, too. The developer can choose whether to render the output in a template file or inline in the ruby code (in the
callmethod). The former style suits well for components with a more complex HTML structure, the latter for smaller and simpler ones.
View Components support and encourage testing via unit tests. The tests are then very fast and validate the rendered HTML output (with support for Capybara matchers included).
Good tests coverage of the view layer is – frankly – not that common in Rails projects because it is notoriously unpleasant. System tests tend to give good coverage but are slow and hard to maintain while view unit tests are possible, as we saw above, but hard to isolate and prepare test data for. View Components profit from their natural encapsulation and explicit data flow, so writing unit tests for them should be much easier.
There is a preview mode for View Components. This is especially handy because it encourages components reuse and provides an obvious place for sample code and documentation.
This all looks very well but did we see any disadvantages before starting with View Components? Not much, really. We expected that building components including meaningful previews and tests definitely requires a bit more work than creating a partial template, for example, but the benefits of doing so, in our eyes, outweighed the pain.
The biggest unclear area that we saw related to view components were forms. There were glimpses of compatibility issues with Rails form helpers in the documentation and we saw a recent effort of the team to mitigate them. Moreover, we were used to building forms with Simple Form which added another variable to the equation. And, in general, we considered the Rails form builders (as well as the Simple Form builder) a system of form-related components in the first place so we were unsure how this would fit into the View Components ecosystem or whether we should even try to do that.
Nevertheless, we decided to try building a few View Components for our new admin interface, and see how it goes.
And yes, we have some findings to share after a few weeks of using and building View Components.
The conventions of View Components seemed so clear and obvious that after a few tries we were able to build new components without hesitation. We routinely added ruby code, templates, unit tests and previews and used the emerging components on the admin pages that we were rebuilding. The feeling that more and more of a page is compiled from a few well-defined and well-tested components is very addictive!
One of the features that we liked the most were previews, especially after we found about the Lookbook project. It is a user interface for viewing, documenting and interacting with View Component previews. The best thing is that Lookbook does not deviate from View Component preview conventions so a developer just has to write a VC preview with perhaps a few optional annotations in the comments and Lookbook automatically converts it into something like this:
We really love Lookbook and it immediately became the official developer version of our ”Design manual“ accessible for everyone in our company.
After a few weeks we realized something unexpected. Building components, especially the more complex and universal ones (that you might expect in an admin interface), felt more like back-end rather than front-end work. Of course, we had to encode the component into a HTML template and style it but this seemed like an icing on the cake. Instead, thinking how to meaningfully pass data into the components and how to interconnect them while still allowing reasonable flexibility became the main focus of our work. Which brings us to the next point…
We put a lot of energy into trying to make the components as easily reusable for the developers as possible. For this we always started by writing code samples that would use the (at that time non-existent) component. We tried a few variants and attempted to cover all the use/edge cases known at that time. Only after we were happy with the external API for the component, we moved on to solving its internals. The result is a set of components that we find lovable to use.
For the components that were meant to be reused frequently, we always added a helper for the sole purpose of simplifying calling the component. For example the
HeadingComponent from the image above, is actually meant to be called via an
admin_heading helper which is just a simple wrapper around the component rendering:
module AdminComponentsHelper def admin_heading(**options, &block) render Containers::Admin::HeadingComponent.new(**options), &block end end
Luckily, View Component previews as well as Lookbook work with helpers without issues so there was nothing stopping us from documenting the actual encouraged style of using the components.
In important conclusion regarding forms came from this effort as well: we decided to not use View Components for forms at all. While we were not particularly happy about having to maintain two different component systems in our code base, we took this pragmatic decision because we like the Simple Form style and Simple Form itself is a very flexible component system, just focused on forms building.
Theoretically, we could be able to mimic the Simple Form API with a set of form-related View Components but we didn’t think the effort was worth it. Instead, we dove deep in Simple Form builders and managed to create a Tailwind-styled one that suits our needs perfectly (this might deserve a separate post; update: there it is). And the best part of all: both unit tests and Lookbook work very well even for Simple Form tests and previews so we didn’t have to compromise anything important. Have a look at this gist for a basic example of such preview.
Overall, we are very happy with adding View Components to our project. Throughout the first few weeks, we built around ten universal components covering most of the needs for our admin pages and are quickly adding new pages in the new style using them. View Components seem like the missing piece that fit perfectly to our current view layer evolution needs.
Since then, the new convention for choosing a pattern in the view layer has become as simple as:
- Is it a form? Use Simple Form with our new helpers.
- Is it supposed to be reusable? Build a View Component and think well about the API and helpers.
- Is it critical? Build a View Component and ensure a good test coverage.
- Is there a non-trivial logic involved in the rendering? Build a View Component.
- None of the above? Choose freely among View Components, templates and helpers, whatever seems like a good fit.
Thank you for your attention, if you feel tempted to try View Components, good!
If you don’t want to miss future posts like this, follow me here or on Twitter. Cheers!