While I'm waiting for tests to pass on my latest PR, I decided to catch up on my inbox. I subscribe to a Ruby newsletter, and I saw this interesting headline: Introducing ActionView::Component: View Components Are Coming to Rails? That had a link to the following PR on Rails:
Introduce support for 3rd-party component frameworks #36388
Note: This PR initially was titled: Introduce support for ActionView::Component
. I've updated the content to better reflect the changes we ended up shipping, to use the new name of GitHub's library, ViewComponent
, and to remove mentions of validations, which we no longer use. - @joelhawksley, March 2020
Introduce support for 3rd-party component frameworks
This PR introduces structural support for 3rd-party component frameworks, including ViewComponent, GitHub's framework for building view components.
Specifically, it modifies ActionView::RenderingHelper#render
to support passing in an object to render
that responds_to
render_in
, enabling us to build view components as objects in Rails.
We’ve been running a variant of this patch in production at GitHub since March, and now have about a dozen components used in over a hundred call sites.
The PR includes an example component (TestComponent
) that closely resembles the base component we're using at GitHub.
I spoke about our project at RailsConf, where we got lots of great feedback from the community. Several folks asked us to upstream it into Rails.
Why
In working on views in our Rails monolith at GitHub (which has over 4,000 templates), we have run into several key pain points:
Testing
Currently, Rails encourages testing views via integration or system tests. This discourages us from testing our views thoroughly, due to the costly overhead of exercising the routing/controller layer, instead of just the view. It also often leads to partials being tested for each view they are included in, cheapening the benefit of DRYing up our views.
Code Coverage
Many common Ruby code coverage tools cannot properly handle coverage of views, making it difficult to audit how thorough our tests are and leading to gaps in our test suite.
Data Flow
Unlike a method declaration on an object, views do not declare the values they are expected to receive, making it hard to figure out what context is necessary to render them. This often leads to subtle bugs when we reuse a view across different contexts.
Standards
Our views often fail even the most basic standards of code quality we expect out of our Ruby classes: long methods, deep conditional nesting, and mystery guests abound.
ViewComponent
ViewComponent
is an effort to address these pain points in a way that improves the Rails view layer.
Building Components
Components are subclasses of ViewComponent
and live in app/components
.
They include a sidecar template file with the same base name as the component.
Example
Given the component app/components/test_component.rb
:
class TestComponent < ViewComponent
def initialize(title:)
@title = title
end
end
And the template app/components/test_component.html.erb
:
<span title="<%= @title %>"><%= content %></span>
We can render it in a view as:
<%= render(TestComponent.new(title: "my title")) do %>
Hello, World!
<% end %>
Which returns:
<span title="my title">Hello, World!</span>
Testing
Components are unit tested directly, based on their HTML output. The render_inline
test helper enables the use of Capybara assertions:
def test_render_component
render_inline(TestComponent.new(title: "my title")) { "Hello, World!" }
assert_text "Hello, World"
assert_selector "span[title='my title']"
end
Benefits
Testing
ViewComponent
allows views to be unit-tested. Our unit tests run in around 25ms/test, vs. ~6s/test for integration tests.
Code Coverage
ViewComponent
is at least partially compatible with code coverage tools. We’ve seen some success with SimpleCov.
Data flow
By clearly defining the context necessary to render a component, we’ve found them to be easier to reuse than partials.
Existing implementations
ViewComponent
is far from a novel idea. Popular implementations of view components in Ruby include, but are not limited to:
In action
I’ve created a demo repository pointing to this branch.
Co-authored-by
A cross-functional working group of engineers and members of our Design Systems team collaborated on this work, including by not limited to: @natashau, @tenderlove, @shawnbot, @emplums, @broccolini, @jhawthorn, @myobie, and @zawaideh.
Additionally, numerous members of the community have shared their ideas for ViewComponent
, including but not limited to: @cgriego, @xdmx, @skyksandr, @jcoyne, @dylanahsmith, @kennyadsl, @cquinones100, @erikaxel, @zachahn, and @trcarden.
Some interesting highlights that I saw were:
Performance
In early benchmarks, we’ve seen performance improvements over the existing rendering pipeline. For a test page with nested renders 10 levels deep, we’re seeing around a 5x increase in speed over partials:
Comparison: component: 6515.4 i/s partial: 1251.2 i/s - 5.21x slower
Rails 6.1.0.alpha,
joelhawksley/actionview-component-demo
, /benchmark route, viaRAILS_ENV=production rails s
, measured withevanphx/benchmark-ips
Testing
ActionView::Component allows views to be unit-tested. Our unit tests run in around 25ms/test, vs. ~6s/test for integration tests.
I've never heard of view components before, at least not in regards to Rails. Seems pretty cool though; what are your thoughts?
Top comments (18)
The concept itself reminds me of Rails cells a company I worked with was using. I remember we hated them because of some issues but the idea behind it was sound. We were using cells only for reusable parts. Shared partials on steroids.
It's not that different from components in the frontend frameworks. You isolate a piece of view's template and behavior (how big depends on you) and you declare what you need in input (like props) and output some HTML.
I really like the idea, for the following reasons mentioned in the GitHub issue:
System tests in Rails are sloooow. I would much prefer using an end to end (e2e) testing framework like Cypress instead of writing tests in Rails. If view components can shave time from that, it's not a bad idea per se. Though e2e tests are not the same thing as isolating a piece of view and testing it.
I see this "problem" in all Rails codebases. We tend to use lots of logic inside views and it's not easy to understand if it's tested or not, you should do it with system/e2e tests if you don't forget. So if you surface views in Ruby objects, code coverage will tell you instantly if that part of the code is at least stressed once or not
This I admit is a pet peeve of mine :D Rails inserts controller instance variables,
@article
for example, inside views. Which is fine for first level views but a very popular convention is to use instance variables in partial, which down the line makes it difficult to understand where that variale comes from when you're debugging.Frameworks like Flask force you to explicitly pass the variables used in the template from the controller. Similar to when you give local variables to a Rails partial. That helps to track down where something comes from.
We adhere to linting tools and quality control but the logic inside views, which is Ruby, is completely ignored by those tools or standards.
One thing I don't like is the possibility of using inline templates, which are basically view helpers
Anyhow, I don't know if this is the perfect or even the right solution, but I support the reasons why they are trying it.
It's weird to me they've decided to insert it in 6.0 which I thought was in feature freeze before the release. This PR is experimental, do they plan to finish the feature before 6.0?
I keep seeing Cyrpus but it is not clicking why I should use it over just writing cucumber tests. Like is it just a plugin that playbacks your steps in the browser so you can exactly see what it's doing. I don't get it.
I guess this explains it well
blog.red-badger.com/2017/6/16/cypr...
There's no mistery to it. You write assertions in JavaScript against any website. It doesn't require the runtime or the language of the website the built in. It runs independently as an end to end test. It has a UI, it doesn't use Selenium which is sloooow, it also doesn't require testers to know anything about your code, they can just take the test suite and run it on their computers.
It looks like they merged it into master and not the 6.0 branch so it should be coming in 6.1.
Oooh this makes much more sense, thank you for checking Abraham! I somehow missed the
Rails 6.1.0.alpha
Andy wrote in his article.hah me too 🙃
I came across this in recent devchat.tv podcast it is so awesome. They also mention about testing.
I love being on the same stack as GitHub now that they are caught up in versions and actively committing upstream. GitHub (e.g. Microsoft) being a Rails shop is huge for the space.
Regarding this feature, it's kind of like view helpers meets partials and much more performant?
Looks neat. I'm mildly worried about too many ways to do the same thing creep, but overall a plus.
I have the same concern about diluting the existing approach. On the other hand I've hand-rolled component-like behavior and conventions in partials enough times that maybe it makes sense to have such a feature built-in (and with a nice testing framework).
Repeating yourself? In Rails? No way lol DHH would never merge that
Very amazing talk that featured this in Railsconf. Great demonstration of TDD and an exciting addition to rails!
Certainly going to give this a spin in my next rails project!
I know that ReactRails/ReactOnRails do something similar where you define components that are rendered on the server through Ruby functions in the controller, but how cool would it be if this Component interface allowed you to specify React components? That way I could write React UI and access the view via a Prop or some sort of Context hook. Whoo!! I'm excited
Xpost from the GitHub thread
Few months ago I rolled out a custom made component solution for a project I'm working on. It's much simpler than this as it still relies on standard rendering pipeline. It's basically a view model that is tightly coupled to a partial and is passed as the only local variable to it when rendered.
It works really well so far, and the resulting code is much easier to understand. I was wondering why nobody else is doing it, but well it seems that a lot of people have been doing it.
I hope that this gets merged to Rails, or at least gets published as a well maintained standalone gem.
That performance increase is 👀👀👀
We already use partials pretty heavily for dev.to. I'm not sure if we have a lot of partials that could be turned into view components, but I'm super interested.
Who the heck nests render 10 levels deep?!
I'm very wary of additional abstractions. Have it as a gem.
In composition? All the time.
If everything is decorating each other, you can get many levels very quickly. Take a list for example.
List
You're at 4 levels deep just from a list. That component would live in some other component, which would probably live in some other component, up until you reach the actual top level component. You could easily reach 10 levels.
There was a session on this at RailsConf this year.
youtu.be/y5Z5a6QdA-M