ViewComponent: My attempt to answer what value it brings

kinopyo avatar

kinopyo

ViewComponent allows you to write reusable and easy-to-test view components in Rails with pure Ruby.

A bit of history

ViewComponent, formerly advertised as ActionView::Component, was first introduced in Railsconf 2019. I got the impression from the talk that it was planned to be merged into Rails. Then this month when I heard the word that it is “supported natively in Rails 6.1,” I thought it’d be available if I upgrade to 6.1. Unfortunately, ViewComponent is still a separated gem and you need to install it manually.

What "supported natively" means is that we can use the gem in Rails 6.1 without a monkey patch. To be exact, any 3rd-party solutions (or any Ruby objects) that have implemented a render_in method can be passed to ActionView render method in Rails 6.1. Check out the original PR for more context.

Even it's not included in the default Rails stack, it still has great potential. If you haven’t watched any of the Railsconf videos, one thing you should know is that ViewComponent is initiated by Github developers. They have been using this approach to construct their views. In other words, it’s been dogfooded on a large scale website and with a large amount of developers, so I believe there is a bright future. 🙂

Installation

If you're on Rails 6.1:

gem "view_component", require: "view_component/engine"

...otherwise, you'll need to put this monkey patch in your application.

How to use

Folder structure

Components are all stored under app/components/ folder. The generator will create corresponding files based on your template engine (ERB, Haml, Slim) and testing framework (TestUnit/RSpec).

bin/rails generate component ModalComponent id
      invoke  test_unit
      create  test/components/modal_component_test.rb
      create  app/components/modal_component.rb
      create  app/components/modal_component.html.erb

Basic form

Say we're creating a modal component, and it accepts an id parameter:

class ModalComponent < ViewComponent::Base
  with_content_areas :header, :body
  
  def initialize(id:)
    @id = id
  end
end
<div class="modal" id="<%= @id %>">
  <div class="header"><%= header %></div>
  <div class="body"><%= body %></div>
</div>

Render the component in a parent consumer view, specifying the container id and content:

<%= render(ModalComponent.new(id: "login-modal")) do |component| %>
  <% component.with(:header) do %>
    Hello Jane
  <% end %>
  
  <% component.with(:body) do %>
    <p>Have a great day.</p>
  <% end %>
<% end %>

Output:

<div class="modal" id="login-modal">
  <div class="header">Hello Jane</div>
  <div class="body"><p>Have a great day.</p></div>
</div>

The content area (header and body) serves like a callback that the parent caller can pass in what content to render, and the modal component gets to decide the markup. It's similar to the named slots in Vue.

Note that there is a more advanced Slots API, currenly in its second iteration, that is supposed to be the successor to with_content_areas. But it's still experimental, you need to keep in mind that it's subject to breaking changes. (It happened to me during the time I wrote this post, the code I wrote a few days ago is now deprecated. 😅)

Sidecar assets

ViewComponent has a concept of Sidecar assets - views and other assets (CSS, JS) can be placed in the same directory to better encapsulate them.

There are two ways to do this.

  1. Keep only the component Ruby file outside of the sidecar directory:
app/components
├── ...
├── example_component.rb
├── example_component
|   ├── example_component.css
|   ├── example_component.html.erb
|   └── example_component.js
├── ...
  1. Keep everything in the same directory:
app/components
├── ...
├── example
|   ├── component.rb
|   ├── component.css
|   ├── component.html.erb
|   └── component.js
├── ...

The JavaScript file can also be a Stimulus controller. Take a look at the doc on Sidecar assets (experimental) to learn how to configure Webpack to compile the sidecar assets.

There are more advanced usages:

  • Conditional rendering
  • Using helper methods
  • Rendering collections
  • before_render hook
  • Inline component (or template-less component)
  • Previews (like ActionMailer preview)
  • Power of composition (render another component inside a component)

Be sure to check out the "building components" section in the official doc to see more examples.

ViewComponent is said to be inspired by React, so if you have some experience with React/Vue, you may find it relatively easy to shift the paradigm of how to write your view logic in Ruby.

What value it brings

The official site already does a great job of selling its greatness - testing, data flow, performance, and standards.

Here are my takeaways.

On testing

Keeping up the test coverage for every possible path of the view with Capybara feature specs is expensive and impractical. ViewComponent makes it easy to test your view logic. Because it’s so easy and fast, both the physical and mental hurdle is almost gone, I believe more developers will feel encouraged to spec up their view components.

I would even say this will drive better design decisions. When I was playing around writing some tests for an existing application, it provided me the opportunity to scrutinize my view logic with fresh eyes, to question myself, why I had added such logic in the first place, would it be possible to simplify it or remove it all. Because adding a test also functions like "locking in" the feature, I'd like to be certain such logic is needed to justify the effort. I can see it would drive such conversations between developers and designers more and come up with better decisions.

Here is an example from a codebase I worked on, it was a delightful experience of writing the test:

require "test_helper"

class CommentPreviewComponentTest < ViewComponent::TestCase
  def test_no_comments
    answer = create(:answer)

    render_inline(CommentPreviewComponent.new(answer: answer))

    assert_link "Leave a comment"
    refute_link "0 Comments"
  end

  def test_one_comment
    answer = create(:answer)
    create(:comment, answer: answer)

    render_inline(CommentPreviewComponent.new(answer: answer))

    assert_link "Comment 1"
  end

  def test_more_comments
    answer = create(:answer)
    create_list(:comment, 2, answer: answer)

    render_inline(CommentPreviewComponent.new(answer: answer))

    assert_link "Comments 2"
  end

  def test_duplicated_commenters_avatars
    answer = create(:answer)
    user = create(:user)
    # setup 2 unique commenters for the answer
    create_list(:comment, 2, answer: answer, user: user)
    create(:comment, answer: answer, user: answer.user)

    render_inline(CommentPreviewComponent.new(answer: answer))

    assert_selector "img", count: 2
    assert_selector "img[alt='#{user.name} avatar']"
    assert_selector "img[alt='#{answer.user.name} avatar']"
  end
end

Running the test:

bin/rails test test/components/comment_preview_component_test.rb
Finished in 1.153699s, 3.4671 runs/s, 6.0674 assertions/s.

On packaging and distribution

ViewComponent is reusable by design. Github's Primer ViewComponents repo is a good example of how they've packaged their design system (Primer CSS) into a ViewComponent-powered gem.

As for the sidecar assets feature that allows you to bundle JavaScript and CSS together, it's still experimental, and I don't know how we can test the JavaScript behavior without sacrificing the speed, but I see great potential in it.

Also, if you happen to use Tailwind CSS and Alpine.js, your HTML markup would work standalone, which makes it much more shareable with ViewComponent. I highly recommend giving these tools a try. 😎

More modern, more powerful

As React and Vue gain more popularity, more and more developers have built the mental model of thinking in the component way. But what Rails provides by default (partial, helpers, layouts) have their shortcomings. I have more than once talked to myself, oh I wish I could just drop in a React component here for this specific view logic, that'd make the code simpler and more elegant.

Finally, now we get the tool!

(There are other libraries solving the view-layer problems such as trailblazer/cells, komposable/komponent, etc. I don't have enough experience to make the comparison 😳.)

Where to begin

If you want to try out ViewComponent in an existing application, here are some starting points:

  • Extract UI based components, such as modal, dropdown, etc.
  • Extract application based components, such as complex views with many inline ruby variables and conditions

Try finding some good candidates and write some tests to feel the joy 😃.

One more thing...

Like the main author @joelhawksley said in one of the Github Issue, ViewComponent probably is not mature enough to be upstreamed yet. It still needs to iterate with the community. In that sense, living in a separated gem makes the feedback loop much faster.

If you were to try it out today, you should be mentally prepared that even though it provides many functionalities, you may need to make more coding decisions at the beginning such as how you want to organize the folder structures, namespaces, naming conventions, where to put certain logics (partial, helper, or ViewComponent object), etc, as real-world code is more complex and sophisticated.

It’d be a good mental exercise, and it’d become easier once we come up with a community best practice. But for now, it’s up to you to make the call. I enjoy this process, but maybe not for everyone. 🙂


I can see myself using ViewComponent as the default choice, I've already installed it in two of my active projects. I’ll share more tips once I get more experience.

Hope you find this post useful. See you next time. 👋

Resources

kinopyo avatar
Written By

kinopyo

Indoor enthusiast, web developer, and former hardcore RTS gamer. #parenting
Published in Rails
Enjoyed the post?

Clap to support the author, help others find it, and make your opinion count.