Rendering view components with Turbo Stream broadcasts

View components, via Github’s view_component gem, are growing in popularity in the Rails community but until recently, view components and Turbo Stream broadcasts didn’t play well together. This made using both view components and Turbo Streams in the same application clunky and a little frustrating.

While there were ways to get components working with streams, thanks to a recent addition to turbo-rails, rendering view components from Turbo Streams now works seamlessly out of the box.

To demonstrate how to connect these two powerful tools together, we’ll be building a very simple Rails application that allows users to manage a list of Spies.

Each spy in the list of spies will be rendered using a view component, and when new spies are added to the database, the newly created spy record will be rendered and broadcast via a callback in the spy model.

When we’re finished, it’ll work like this:

Beautiful, I know.

This article assumes that you’re comfortable building Rails applications. You won’t need any previous experience with Turbo Streams or view components to follow along.

If you want to skip right to the end the complete code for this article can be found on Github.

Let’s start building!

Application setup

We’ll be working from a fresh Rails application. I’m working off of the latest Rails 7 release (alpha2 at the time of this writing) but everything in this tutorial will work fine on Rails 6.1 too.

If you want to follow along, you can run the below commands from your terminal, or you can clone this Github repo and skip ahead to the Building the spy component section.

rails new turbo-view-components -T
cd turbo-view-components
rails g scaffold Spy name:string mission:string
bundle add view_component
rails db:create db:migrate

If you’re using Rails 6.1 instead of 7, you’ll also need to install turbo-rails manually.

bundle add turbo-rails
rails turbo:install

Whichever route you choose, you’re ready to move on to the next section once you have view_component and turbo-rails installed in your application and a Spy resource created. Once you're setup, start up your server with rails s and head to http://localhost:3000/spies.

Building the spy component

To use view components, we need to create a component to render a spy object. We can create new components with the built-in generator view_component provides:

rails g component Spy spy

This generator will create both a spy_component.rb file and a spy_component.html.erb file in the components directory.

Since we're building a very simple component, spy_component.rb is good to go out of the box. For reference, it should look like this:

class SpyComponent < ViewComponent::Base
  def initialize(spy:)
    @spy = spy
  end
end

spy_component.html.erb contains placeholder content provided by the generator right now, so let’s update it to render information about each spy:

<div id="<%= dom_id @spy %>">
  <p>
    <strong>Name:</strong>
    <%= @spy.name %>
  </p>

  <p>
    <strong>Mission:</strong>
    <%= @spy.mission %>
  </p>

  <p>
    <%= link_to "Show this spy", @spy %>
  </p>
</div>

This is just regular old erb, essentially a copy of the default content of _spy.html.erb generated by the Rails scaffold generator we ran during application setup.

With the spy component in place, we now need to actually use it. To do that, we’ll update the spies index view to use the our new component. Update spies/index.html.erb like this:

<p id="notice"><%= notice %></p>

<h1>Spy</h1>

<div id="spies">
  <%= render(SpyComponent.with_collection(@spies)) %>
</div>

<%= link_to "New spy", new_spy_path %>

Here we’re using collection rendering to loop through each spy in @spies and render each with spy_component.html.erb.

We’ve now got our list of spies rendering using view components — next up we’ll add Turbo Stream broadcasts so that newly created spies are appended to the list automatically.

Add Turbo Stream broadcasts

First, we need to ensure that visitors to the spies index page are subscribed to the appropriate turbo stream channel.

To do this, we can use the turbo_stream_from helper from turbo-rails. Update spies/index.html.erb like this:

<!-- Snip -->
<%= turbo_stream_from "spies" %>
<div id="spies">
  <%= render(SpyComponent.with_collection(@spies)) %>
</div>
<!-- Snip -->

There are two important pieces here. First, we added the turbo_stream_from helper, with spies as the name.

This helper creates a <turbo-cable-stream-source> in the rendered HTML with a signed-stream-name that looks something like this:

<turbo-cable-stream-source channel="Turbo::StreamsChannel" signed-stream-name="aLongSecureName"></turbo-cable-stream-source>

Next, the div wrapping the list of spy components has an id of spies. This id must match the target of the Turbo Stream broadcast, which defaults to the plural name of the model we are broadcasting from. If our wrapper div doesn’t have an id, the broadcast we add next will fail.

With the stream subscription added to the view, the last step is to add a model callback in models/spy.rb to broadcast newly created spies on the spies channel.

Update spy.rb like this:

class Spy < ApplicationRecord
  after_create_commit :append_new_record

  private

  def append_new_record
    broadcast_append_to(
      'spies',
      html: ApplicationController.render(
        SpyComponent.new(spy: self)
      )
    )
  end
end

Here we’re using the new html option added to Turbo Stream broadcast methods to render the SpyComponent instead of rendering a partial.

Note that using ApplicationController.render to render a view_component isn’t officially sanctioned by the view_component team.

The view_component folks are actively discussing an official way to add support for stream broadcasts, which you can track on this issue.

With the model broadcast in place, we’re ready to test our Turbo Stream-enabled view components.

A note for Rails 7 users: If you’re on Rails 7 alpha2 (the latest release at the time of this writing) an issue exists that will prevent the broadcast from working because of a disabled session error. This issue is entirely unrelated to view components but it will break our broadcast all the same.

This issue will be fixed in the next Rails release, but until then you can prevent the issue by updating development.rb with this line:

config.action_controller.silence_disabled_session_errors = true

To see it action, refresh the index page, and then open a new tab to http://localhost:3000/spies/new, create a new spy, and see that the newly created spy is automatically appended to the list of spies automatically.

Wrapping up

Today we looked at a technique to render View Components from Turbo Stream broadcasts, leveraging the recently added ability to render html instead of a partial from Turbo Stream broadcasts.

This article’s focus is on Turbo Stream broadcasts + view_component, so we didn’t dive deep into how Turbo Streams work or how to take full advantage of the real power of view_component.

To dig deeper, you might find these resources helpful starting points:

  • The view_component documentation is excellent, and worth reviewing in detail to understand more about how you can use view components
  • Encapsulating Ruby on Rails views from Github’s blog is a nice introduction to the work behind the ViewComponent library and includes links to see how Github uses ViewComponents in their application
  • The Turbo Rails source code. If you’re serious about using Turbo in your Rails application, thoroughly reviewing the source and accompanying comments is highly recommend.
  • The Turbo handbook is the best place to start if you’re new to Turbo and want to understand the basics of Streams, Frames, and Drive

That’s all for today. As always, thanks for reading!

47