loading...

Rails to Introduce View Components

andy profile image Andy Zhao (he/him) ・1 min read

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, via RAILS_ENV=production rails s, measured with evanphx/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?

Discussion

pic
Editor guide
 

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:

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.

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.

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.

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

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.

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.

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

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...

 

I keep seeing Cyrpus but it is not clicking why I should use it over just writing cucumber tests.

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.

 

I came across this in recent devchat.tv podcast it is so awesome. They also mention about testing.

 

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.

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!

 

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.

 

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

 

Who the heck nests render 10 levels deep?!

For a test page with nested renders 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

  • ListItem
    • ListItemAvatar
      • Avatar
    • ListItemText
      • ListItemPrimaryText

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.

 

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.

 

There was a session on this at RailsConf this year.
youtu.be/y5Z5a6QdA-M