Bringing component architecture to server-rendered Rails

Rails 6.1 quietly added support for rendering objects that respond to render_in, a small API change with large implications for view architecture. That addition paved the way for ViewComponent, a framework GitHub built for creating reusable, encapsulated view components in server-rendered Rails applications.

Traditional Rails views lack encapsulation. Every template in an application shares a single execution context, so views can leak state into one another. That makes them difficult to reason about and expensive to test in isolation. As GitHub's template count grew into the thousands, the team found itself depending on presenters and partials with inline Ruby, all covered by slow integration tests that exercised routing and controllers just to verify markup.

The motivation came from React: component-based UI with clear boundaries and testability. ViewComponent translates those ideas to server-rendered Rails.

How ViewComponent works

A ViewComponent is simply a Ruby class paired with a template file. The class declares initializer parameters and the template is rendered in the context of the component instance.

test_component.rb

class TestComponent < ViewComponent::Base
  def initialize(title:)
    @title = title
  end
end

test_component.html.erb

<span title="<%= @title %>">
  <%= content %>
</span>

Rendered in a view:

<%= render(TestComponent.new(title: "my title")) do %>
  Hello, World!
<% end %>

Returning:

<span title="my title">Hello, World!</span>

Because the template executes within the component object's context, each component's state is isolated. That isolation enables direct unit testing without going through the routing and controller layers:

require "view_component/test_case"

class MyComponentTest < ViewComponent::TestCase
  def test_render_component
    render_inline(TestComponent.new(title: "my title")) { "Hello, World!" }

    assert_selector("span[title='my title']", text: "Hello, World!")
    # or, to just assert against the text:
    assert_text("Hello, World!")
  end
end

The difference in test speed is dramatic. In GitHub's codebase, a component unit test runs in roughly 25 milliseconds; a controller test takes about six seconds.

Real-world impact at GitHub

Adoption has been steady: more than 400 components now render across 1,600 of GitHub's 4,500+ templates. Primitive UI elements such as the Counter and Blankslate components are powered by ViewComponent on GitHub.com.

Three benefits stand out in practice. First, component tests target the rendered DOM directly, reducing duplicated coverage that previously lived in controller tests. Second, the low cost of running component tests encourages more of them, improving confidence in view code. Third, component-driven structure enforces consistency: when the team built a ViewComponent to display pull request status, it uncovered places where copied implementations had not been updated to handle the draft pull request state. A single shared component eliminates that class of drift.

The components used in the GitHub application are available in the Primer ViewComponents library. The Rails 6.1 render_in support is one of several contributions GitHub made to that release, alongside horizontal sharding, strict loading, and template annotations.