---
title: 'Hotwire: Turbo-Streaming ViewComponents'
teaser: 'Learn how to Turbo-Stream ViewComponents by building a message board.

  '
tags: hotwire,turbo,viewcomponent,rails
author: Connor McQuillan
published_on: 2022-02-08
---

GitHub built the [ViewComponent] library to integrate with Ruby on Rails. It
exists as a framework for creating reusable, testable, and encapsulated view
components. GitHub’s [long term goal] is to build all their Rails-rendered HTML
with ViewComponents.

Rails 7 ships with Hotwire - Turbo included - [by default]. Turbo Streams make
up part of Turbo and deliver page changes as fragments of HTML “over the wire”
(e.g. with web sockets). They allow you to bring your application to life and
keep users up to date with changes, all achieved without writing any JavaScript
whatsoever.

But Turbo Streams lean on Rails partials. ViewComponents are reusable, testable
and [more performant] than partials. How can we integrate them with Turbo
Streams so we can use them in place of traditional partials?

Let’s give it a try.

All the source code used to create this post is [on GitHub]. The `rails new`
command with the latest version of Rails generated the code in this project.

[ViewComponent]: https://viewcomponent.org
[long term goal]: https://viewcomponent.org/viewcomponents-in-practice.html
[by default]: https://github.com/rails/rails/pull/42999
[more performant]: https://viewcomponent.org/#performance
[on GitHub]: https://github.com/cpjmcquillan/hotwire-viewcomponent-example

## Building a message board with ViewComponent

Let’s consider a basic messages page, built using ViewComponent. Each message
displays the message body and the created at timestamp in a readable format.
There is also a small piece of logic included that highlights the most recent
messages in green.

```ruby
# app/controllers/messages_controller.rb

def index
  @messages = Message.all
end
```

```erb
# app/views/messages/index.html.erb

<div>
  <h1 class="text-xl font-bold">Messages</h1>

  <div class="max-w-2xl">
    <%= render MessageComponent.with_collection(@messages) %>
  </div>
<div>
```

Like everything else in Ruby, ViewComponents are objects. That means we can keep
our views clean and tidy by moving Ruby code and logic into our component file.

```ruby
# app/components/message_component.rb

class MessageComponent < ViewComponent::Base
  include ActionView::RecordIdentifier

  def initialize(message:)
    @message = message
  end

  private

  attr_reader :message

  delegate :body, :created_at, to: :message, prefix: true

  def recent_message?
    message_created_at > 1.hour.ago
  end

  def timestamp
    message_created_at.strftime("%Y-%m-%d at %H:%M")
  end
end
```

```erb
# app/components/message_component/message_component.html.erb

<%= tag.div(id: dom_id(message), class: "message-component flex flex-wrap justify-between items-center py-3 border-b-2 border-gray-300") do %>
  <%= tag.div(
        message_body,
        class: class_names("mr-10", "text-green-600" => recent_message?, "text-gray-800" => !recent_message?)
      ) %>

  <%= tag.time(timestamp, class: "text-gray-400") %>
<% end %>
```

## Integrating with Turbo Streams

### Posting messages

A form on the messages page and a create action in the messages controller post
new messages. With Turbo Streams we can render new messages in the response
without re-rendering the whole page. Turbo tag helpers allow us to replace the
form and append the new message to the list of messages.

```diff
--- a/app/views/messages/index.html.erb
+++ b/app/views/messages/index.html.erb

<div class="max-w-2xl">
  <h1 class="text-xl font-bold my-5">Messages</h1>

  <div id="messages" class="mb-5">
    <%= render MessageComponent.with_collection(@messages) %>
  </div>
+
+ <%= render "form" %>
</div>
```

```erb
# app/views/messages/_form.html.erb

<div class="my-3">
  <%= form_with url: messages_path, id: "new_message_form" do |f| %>
    <%= f.label :body, "Message:", class: "mr-2" %>
    <%= f.text_field :body, placeholder: "What do you want to say?", class: "my-2 mr-2" %>
    <%= f.submit "Post message", class: "p-2 cursor-pointer hover:bg-green-600" %>
  <% end %>
</div>
```

```diff
--- a/app/controllers/messages_controller.rb
+++ b/app/controllers/messages_controller.rb

class MessagesController < ApplicationController
  def index
    @messages = Message.all
  end
+
+ def create
+   @message = Message.create(body: params[:body])
+
+   respond_to do |format|
+     format.html { redirect_to messages_path }
+     format.turbo_stream
+   end
+ end
end
```

```erb
# app/views/messages/create.turbo_stream.erb

<%= turbo_stream.replace "new_message_form", partial: "form" %>

<%= turbo_stream.append "messages" do %>
  <%= render MessageComponent.new(message: @message) %>
<% end %>
```

### Broadcasting messages

What about when other users post messages?

The turbo-rails library has excellent integration for [broadcasting]
ActiveRecord objects with Rails partials. `Turbo::StreamsChannel` allows HTML to
be broadcast outside a model. We can use `ApplicationController.render` to
render the ViewComponent as HTML that we can pass to Turbo. Now we have a class
that we can use to broadcast messages from the messages controller.

```ruby
# app/models/broadcast/message.rb

module Broadcast
  class Message
    def self.append(message:)
      new(message).append
    end

    def initialize(message)
      @message = message
    end

    def append
      Turbo::StreamsChannel.broadcast_append_later_to(
        :messages,
        target: "messages",
        html: rendered_component
      )
    end

    private

    attr_reader :message

    def rendered_component
      ApplicationController.render(
        MessageComponent.new(message: message),
        layout: false
      )
    end
  end
end
```

```diff
--- a/app/controllers/messages_controller.rb
+++ b/app/controllers/messages_controller.rb

def create
- @message = Message.create(body: params[:body])
+ @message = Message.new(body: params[:body])
+
+ if @message.save
+   Broadcast::Message.append(message: @message)
+ end

  respond_to do |format|
    format.html { redirect_to messages_path }
    format.turbo_stream
  end
end
```

[broadcasting]: https://github.com/hotwired/turbo-rails/blob/main/app/models/concerns/turbo/broadcastable.rb

## Looking back

We built a basic message board that renders new messages without the need to
refresh the page.

We leveraged ViewComponents to build a reusable message component.

We integrated with Hotwire and Turbo Streams to broadcast messages to users in
real time.

And we did all this without writing any custom JavaScript!

![Broadcasting messages using Turbo Streams](https://images.thoughtbot.com/blog-vellum-image-uploads/AxHKhfDSKWlJqvlbaHLw_Hotwire-ViewComponent_delay.gif)
