ViewComponent in the Wild I: building modern Rails frontends
Translations
GitHub’s ViewComponent library has been around for some time, helping developers stay sane while building the view layer of their Ruby on Rails applications. It’s steadily grown in popularity—but not as quickly as probably deserved. In this series, we’ll lay out why you need to give it a shot, too. We’ll discuss some best practices and examine various tips and tricks that we’ve accumulated in our ViewComponent-powered projects at Evil Martians.
Other parts:
- ViewComponent in the Wild I: building modern Rails frontends
- ViewComponent in the Wild II: supercharging your components
- ViewComponent in the Wild III: TailwindCSS classes & HTML attributes
In this part, we’ll just dip our toes into the wilderness, focusing on the higher-level aspects of applying the component approach when building view layers inside modern Rails applications. You’ll learn why this approach is a great alternative to building single-page applications—despite its relative novelty and (seemingly) still scarce adoption rate. And as mentioned, crucially, you’ll also learn how to properly apply this approach—without going insane.
There will be a minimum amount of code to reckon with here since we’ll try to keep our eyes on the big picture, but if you’re a seasoned developer who’s only interested in the nitty-gritty of setting things up and you’re ready to venture far into the wild right away, feel free to skip ahead to the next part where you’ll find those juicy, born in production code snippets you’re looking for. 😉
Table of contents:
Redeeming the ‘V’ in MVC 🌱
But first, let’s take a step back and address a legitimate question that’s probably at the back your mind: “why?” . Why do we need to bother ourselves with the knowledge of building the view layer in classic MVC applications when it’s 2022 and it seems like no one is doing this stuff anymore? 🤔
Indeed, at present, almost every new project you’ll come across has a separate frontend application with a separate team of frontend engineers maintaining it. And there’s a good reason for that: frontend development has grown and matured a lot in recent years; we can now build applications with previously-unimagined levels of complexity. And yet, at what cost?
As you might have guessed, the answer to this question lies in the simple fact that we’re forced to maintain two separate applications (one for backend and one for frontend). This could easily double development costs: more code needs to be written, which means more engineers need to be hired, which ultimately means more money needs to be spent. (And I’m not even mentioning hidden costs: these range from obvious things like more complex infrastructure, to less obvious concerns like additional difficulties setting up proper team processes and communications).
Separate engineers → bigger teams → larger development costs (both time and money).
So, I’d argue the proper question to ask isn’t “why should I use a classic MVC approach?”, but “why shouldn’t I?” 😉
Consider your particular case: does your project really need to be a single-page application, or would it be overkill?
The better question to ask is this: why shouldn’t we use classic, server-driven MVC?
Are React/Vue.js/Svelte/etc. the only ways to build modern, responsive web applications? Of course not! And we don’t need to look far for an example: GitHub. That’s right, the app powering most open source development today is a multi-page Rails application, and it still uses ERB templates to render most views. I don’t know about you, but in my view, it’s one of the most reliable, easy-to-use web applications—and this comes despite its complexity and the fact that it still continues to grow and improve.
If the classic, server-driven MVC approach works for a massive application like GitHub, there’s a chance it could work for you, too!
In fact, you may even be at an advantage compared to GitHub because you can make use of the new tools available right from the start. For example, recently, a new approach has started gaining popularity in the development community: HTML over-the-wire. Pioneered by Phoenix LiveView, it took the Rails world by storm with Hotwire, which promises a simple way to build responsive web interfaces without a single line of JavaScript. Of course, it’s not a silver-bullet, but it’s definitely a viable alternative to building an SPA for a wide range of projects alongside the obvious advantages I quickly outlined above.
I highly recommend checking out the awesome Frontendless Rails Frontend presentation by Vladimir Dementyev if you yearn to learn more.
Anyway, while it’s up for debate whether or not a specific project needs to be an SPA or a classic multi-page application, one thing is for certain: both approaches need a sane and maintainable way to structure their code. This finally brings us to the subject at hand: ✨components✨.
Components with benefits 🌿
Frontend development has been “componentized” for such a long time that no one even questions this approach anymore. And it’s clear why: components are the embodiment of what all good code should be—isolated, easily testable, reusable, and composable (duh). Compared to the jQuery-driven spaghetti code we were writing over 10 years ago, it’s like driving a tricked-out DeLorean after years of riding a horse. No wonder the pioneering solutions of this approach (namely, React and Vue.js) were so quickly and widely adopted by the industry.
And yet, when you rails new
a project, what do you see? Partials and view helpers—like it’s 2005 all over again. It’s been said time and again that this approach is badly outdated because it violates even the most basic standards of code quality. And, just like with jQuery-spaghetti, all the drawbacks of this approach are still here: tightly-coupled code, implicit arguments, unpredictable dataflow, magic constants—and don’t even get me started on testing!
Now, if this all sounds exactly like the issues the frontend industry was suffering from back in the day—that’s because they are. The only difference is that, up until recently, us backend developers have been ignoring them due to specifics of our work (and also due to the fact that frontend development has taken over). But really, there’s nothing inherently wrong about the classic approach of rendering HTML on the server (which is actually a funny thing to say since HTTP literally stands for Hypertext Transfer Protocol).
The Rails view layer was never a lost cause: all it needs are better tools.
And that’s where the ViewComponent library comes in! It’s a Ruby-native implementation of the component pattern we’ve all been missing that solves 100% of the issues above, and then some. At its core sits a deceptively simple idea that nevertheless has far reaching consequences: a view component is just a Ruby object with an associated template (ERB/Slim/etc.)
And all you need to render a component is to instantiate it, just like any other object, by passing the required dependencies as arguments to the constructor, and shoving it to Rails’ #render
—that’s it.
That doesn’t sound so different from using partials, now does it? It almost feels like we’re just manually carrying out what partials do under the hood anyway. And yet, by instead choosing to use plain Ruby objects and making explicit that which was formerly implicit, we gain a lot. Most importantly—we gain predictability (which essentially translates to maintainability). We bring to our views the full power of arguably Ruby’s strongest feature-its OOP.
But wait, that’s not all! For example, let’s look at how this affects testing. Considering the definition above, I doubt it will surprise you that testing components is as easy as testing POROs (if you don’t believe me, read on and assuage your doubts). With ViewComponent, you can finally be confident in your view code and stop trying to fix holes in the test suite with numerous request and/or system tests—which are notoriously slow, brittle, and of which there never seem to be enough (because integrated tests are a scam). Thus, a healthy test pyramid no longer sounds like a mythical concept achievable only in theory, but something we can obtain in actual reality.
Oh, and did I mention that component tests are like, crazy fast? Testing the DOM with ViewComponent is basically free. So, if at this point you’d still insist on using partials, well… all I can say is, good luck testing a conditional branch somewhere deep down the view chain (and may the force be with you)!
There are, of course, other (slightly less obvious) benefits to using components on the backend, and I believe one of these in particular deserves our attention.
The biggest benefit of using view components is parity between backend and frontend teams.
Frontend developers are used to “thinking in components”. And, speaking from experience, having the same approach on the backend drastically reduces the learning curve when the need to dive into backend code to fix some views arises (and, let’s be honest, the necessity does crop up sooner or later).
All in all, it’s no secret that the Rails asset pipeline has improved a lot lately (with tools like Propshaft, esbuild, Vite, and so on). It’s safe to say it has caught up with frontend tools in terms of features and usability. The only thing that was missing to be able to play in the same field as frontend developers was proper design patterns, and I believe that ViewComponent finally bridge that gap. And so, hopefully, I’ve already been able to convince you that view components on the backend are definitely worth a try!
How to ✨component✨ 🌲
Now that I’ve (hopefully) sold you on the general idea, it’s time to confront a hard-to-swallow pill: unfortunately, the simple act of using components won’t just magically solve all your problems. We can’t just slap them on top of our existing view code and expect to immediately make it better. There are rules.
As with any architectural pattern, the component approach requires a certain knowledge on how to operate (or rather, how not to operate) within this approach in order to actually make it beneficial. Otherwise, you might end up with the same legacy code, but this time it would be “clever” legacy code (the worst kind).
The good news is that the knowledge required is already available to us backend engineers in abundance! That’s all thanks to the diligent efforts of our frontend engineer friends who have meticulously crafted the list of best practices while simultaneously battle-testing that list across numerous production environments over the past 10 years. All we’re got to do is to adopt and adapt it into our reality. In other words, since the wheel has already been invented, all we’ve got to do is to roll with it.
Now, obviously there will be some differences (and that’s why I’m writing this instead of simply pointing you towards React’s documentation), but the core idea behind every best practice here will always remain the same. Thus, I urge you to take a step back when reading the code snippets, skim over the details, and focus on the overall idea instead.
This will not only help you better understand the principles behind the component approach, but it will also spare you quite a bit confusion as some snippets use a few “unorthodox” features that you won’t find in ViewComponent documentation. Don ’t worry! Everything will be cleared up in the next part, but for now, let’s try to keep our eyes on the big picture.
Test the actual behavior, not the helpers
We already touched on the topic of testing earlier, and we established that testing components is a 🍰, but how exactly do we test them? Or rather, what do we test? Well, just like with any other Ruby object, we test the public interface—which in case of a component is (surprise, surprise) its template.
It’s tempting to write tests for the methods defined in the component’s Ruby class because it feels easy and familiar, but if you think about it, essentially, that’s the same as writing tests for private methods (please don’t do that). Treat those methods as nothing more than helpers and you’ll be alright.
“Okay, but how do we test a template?” I’m glad you asked!
Let’s look at the following example:
<!-- app/views/components/menu/component.html.erb -->
<% if current_user %>
<div class="greeting">Hello, <%= current_user.name %>!</div>
<%= button_to t(".sign_out"), users_sessions_path, method: :delete %>
<% else %>
<%= button_to t(".sign_in"), users_sessions_path %>
<% end %>
What do you see? I, for one, see imperative code with conditional logic that just begs to be covered with tests. Perhaps the naming is a bit confusing because a template is supposed to be something declarative (and it is to some degree), but at the core, it’s not that different from the regular code we write, and accordingly, it has to be tested in a similar way.
This is how tests for this component might look:
# spec/views/components/menu_spec.rb
describe Menu::Component do
subject { page }
let(:component) { described_class.new }
before do
with_current_user(user) { render_inline(component) }
end
context "when current_user is present" do
let(:user) { build(:user, name: "Handsome") }
it "renders sign out button" do
is_expected.to have_link "Sign out"
end
it "has greeting text" do
is_expected.to have_content "Hello, Handsome!"
end
end
context "when current_user is absent" do
let(:user) { nil }
it "renders sign in button" do
is_expected.to have_link "Sign in"
end
end
end
Note: we don’t make assertions for the exact markup—there’s no point. Instead, we’re interested in the same things as when writing any other unit test: conditional logic and calculations (for the sake of simplicity, it’s just string interpolation here). This is how you get good test coverage for your view layer.
ViewComponent provides us with a straightforward way to test our components in isolation so we can finally write view code that we can actually trust.
And look how blazingly fast these tests are. That’s all because we test the static output without making HTTP requests or setting up a full-blown browser… wait, static? But what about dynamic JS behavior? Surely we would want to sprinkle our components with JS at some point!
While ViewComponent’s maintainers are looking to merge a PR allowing us to easily test view components’ dynamic behavior in isolation while working in a real browser, you can achieve the same with a preview and a classic system test. All you need to do is to load the preview page in your test:
# spec/system/components/my_component_spec.rb
it "does some dynamic stuff" do
visit("/rails/view_components/my_component/default")
click_on("JavaScript-infused button")
expect(page).to have_content("dynamic stuff")
end
Use context to pass global state
Components need data to render, but where does that data come from? If we rule out esoteric options like pigeon post or (heaven forbid) global variables, we’re left with only one way to pass data to components: arguments. However, since data is passed top-down, this will get pretty cumbersome for “popular” data (like the current user, for example) since we must manually pass it down every level of the component tree just to get it to the actual component in question.
Yikes! How do we fix this? If you’re familiar with frontend development you already know the answer: context.
Instead of injecting dependencies explicitly, we can inject them implicitly via context—a shared object that can be accessed by any component in the component tree.
One way to do that is dry-effects which is an implementation of algebraic effects for Ruby. Don’t sweat the big-brain words; it simply allows you to set a value somewhere up the call stack and access it from anywhere down the line (e.g. set current_user
in the controller and access it in your view components).
In the next part, I’ll show how to do exactly that. For now, just be aware of this technique and keep in mind that it shouldn’t be abused. The more things you make implicit by passing them via context, the harder it gets to track where the data came from and account for it when writing tests.
Avoid deeply nested component trees
Matryoshka dolls are fun because you keep wondering how much deeper they can go. Similarly, when working with deeply nested component trees, you also wonder how much deeper you can go (until you’ve completely lost your marbles, that is). So, to avoid unnecessary injuries, let’s find out why and how to avoid such situations.
We already know how to pass frequently used data down the component tree using context, but obviously not all data is passed this way. In fact, most of the time it comes from arguments, so the issue we’ve outlined in the previous section still stands.
In cases where passing data via context is not applicable, the solution to the “argument drilling” problem is to pass down components instead of data.
ViewComponent provides a couple ways to do this: the content
accessor and slots. Let’s take a look at an example of a component using slots:
# app/views/components/feed/component.rb
class Feed::Component < ApplicationViewComponent
renders_one :pinned
renders_many :posts
end
<!-- app/views/components/feed/component.html.erb -->
<div class="pinned">
<%= pinned %>
</div>
<% posts.each do |post| %>
<%= post %>
<% end %>
And this is how you render it:
<%= render(Feed::Component.new) do |c| %>
<% c.with_pinned do %>
<%= render(Post::Component.new(@pinned_post)) %>
<% end %>
<% @posts.each do |post| %>
<% c.with_post do %>
<%= render(Post::Component.new(post)) %>
<% end %>
<% end %>
<% end %>
Oof, that’s a lot of code! If you compare it to how it’d look if we’d decided to pass data through arguments instead, you might come to the conclusion that it’s not worth it:
<%= render(Feed::Component.new(pinned: @pinned_post, posts: @posts)) %>
But consider this: what if we had two feeds in our application (one personal and one global) and wanted the visual appearance of the post
component to be slightly different between the two? (For instance, hiding the post author in personal feed.) Of course, we’d need to add a new option to our post
component (show_author
or something), but the feed
component wouldn’t know what to set it to when rendering post
components unless we add the same option to the feed
component as well. Imagine you had to do the same for every component up the tree—that’s enough to make the most reasonable person go mad.
Whenever a child component has data requirements that are different from its parent, it almost always means that it should be passed down as a component instead of being hardcoded in the parent component’s template.
This technique will save you a lot of headaches, but not only that: it will also make your components more reusable. What if we wanted to show a different component in the pinned
slot? Or swap the feed
with another component with the same slots? (Like a board
or something.) With this approach you basically get it for free.
Extract general-purpose components
If you’ve been dwelling in frontend circles for some time, you’ve probably heard about “Smart” and “Dumb” components (or alternatively, Presentational and Container components). You could also call them general-purpose and app-specific components. Regardless of the name, the former are supposed to act as your application’s “palette” (while being ignorant of the application’s data model) and the latter are the ones using this palette to actually paint the view. Presentational components are concerned about how things look, Container components are concerned about how things work—you get it. Basically, if you pass an ActiveRecord
object to a component, then it’s definitely app-specific.
Separating general-purpose and app-specific components gives you a higher chance of being able to reuse view code across the application and to “DRY it up” in the process.
While it’s probably not worth it to stick to this separation adamantly, I still think it’s important that you think about what the core components of your application are because this will help you keep the UI consistent. And maybe one day, you can even open source it (like GitHub did with their Primer ViewComponents).
Stick to the single-responsibility principle
Just as a thousand-line-long God-model is a code smell, so is a thousand-line-long view template. Actually, in the case of templates, even a hundred lines gives off a serious stench because (as opposed to the methods inside a class) everything in a template is connected—at the very least through a parent-child relationship, but it rarely ends there.
You’ve probably heard of “low coupling and high cohesion” (basically boiling down to the good ol’ principle of “divide and conquer”) that always springs up when it comes to modular architecture of monoliths. Well, guess what? View components are no different. Modules, blocks, components: call it however you like, the core approach remains the same.
Remember that first and foremost, we use components to promote good standards of code quality in our codebase.
That means sticking to the same principles we always do (or trying to, at least). This especially applies to the single-responsibility principle.
Seriously though, large component trees are looked down upon in the frontend community. There are good reasons for that: they’re difficult to understand and refactor, and they’re almost impossible to reuse. With time, their implementation gets so entangled due to nearby component logic gradually becoming coupled, it might be easier to trash the whole component subtree altogether and re-write it from scratch.
Components should be atomic and only concerned with a single responsibility.
Decomposing large component trees like that takes time and effort, but if your goal is to keep complexity at bay, then it’s 💯% worth it.
Avoid making database queries inside components
This one is specific to backend development.
Views are for rendering data, not fetching it.
Instead of views, fetch data in controllers. You wouldn’t, for example, eat in your bed, right? Oh, you would? Fine, me too—but that’s not the point! What I mean to say is that we need a clear separation of concerns to make our life easier. Like, every time I let myself eat a snack in bed, there are a bunch of difficult-to-get-rid-of crumbs left lying around. And just as crumbs in the bed are annoying, so too are the N+1 issues that, without failure, arise when you decide that making database queries in your views is a good idea. Try to avoid this when possible and preload the data preemptively.
P.S. Even though cookies make a ton of crumbs, ice cream doesn’t make any. (This isn’t some component metaphor, just a tip for piggin’ out in bed.)
In fact, as a proactive measure, you could even go a step further and completely prohibit your components from making any database queries during development. I’ll show how to set this up in the next part, so stay tuned! 😉
Whew! I think we’ve covered most of the nasty pitfalls that await those who decide to embark on the path of adopting the component approach. Keep in mind that there are no hard rules here (every case is different!), but generally speaking if you stick to the above guidelines it will result in a better, more enjoyable, and easily maintainable code.
Now, the next step is finally setting foot into the wilderness that is ViewComponent production usage, and making it our own!
Enough talk, let’s get down to code! 🌲🌳🌲🌳🌲