Hotwire has been around for a while now. If you got a chance to create an app powered by it yet, you must have realized how easy it is to create apps that feel like single-page applications. In this blog, we will see the lesser-discovered part of the ecosystem - broadcasting.

Turbo streams

Think of turbo streams as a set of changes to perform on your UI. You send these changes back to the UI as a response to an event, form submission, or whenever you feel like it. If you are new to this, I recommend going through the Turbo - The heart of Hotwire first.

Broadcasting?

If you are creating an application where you want changes in the UI to be reflected not just for the instance that requested it but for others (who're viewing the same page) too, then broadcasting is what you need.

Let's take a simple example of a notifications feature.

Image depicting change in notification icon 

How would you make sure that if an event occurs, the notification icon updates for everyone? You can create a turbo stream that replaces the icon and send that turbo stream to everyone.

First, everyone has to listen to a WebSocket channel for incoming turbo streams. This is done using

<%= turbo_stream_from(:notifications) %>

where :notifications is just the name of the channel.

Second, you broadcast the update using the special broadcasting methods provided by the Turbo::Broadcastable concern.

broadcast_replace_to(:notifications, target: "icon", html: "<span class='icon'>notification_new</span>")

This method does two things

  1. Generates the turbo stream.
  2. Broadcasts the turbo stream to all subscribers of the notifications channel.

The broadcasted turbo stream looks something like this:

<turbo-stream action="replace" target="icon">
    <span class='icon'>notification_new</span>
</turbo-stream>

You can call this function in the controller or in the model after a callback like:

# app/models/Post.rb

class Post < ApplicationRecord
  ...
  after_create_commit -> { 
    broadcast_replace_to(
      :notifications,
      target: "icon",
      html: "<span class='icon'>notification_new</span>"
      )
  }
  ...
end

Once a post is created, the icon for all the users will be replaced with a new one indicating a new notification.

The ways of Broadcasting

So far, you have seen one type of broadcasting - replace. There are broadcasting functions for every type of turbo stream actions -  broadcast_update_to, broadcast_append_to ... the entire list is here. Let's see some of the common hurdles that you might face while using them.

Multiple Turbo stream actions at once?

If you want to broadcast multiple turbo stream actions at once, you can put all of your actions in one *.turbo_stream.erb file and broadcast the file!

# app/views/notifications/_new.turbo_stream.erb

<%= turbo_stream.replace "icon" do %>
   <span class='icon'>notification_new</span>
<% end %>
<%= turbo_stream.append "icon" do %>
   <span class="count"><%= count %></span>
<% end %>
# app/models/Post.rb

class Post < ApplicationRecord
  ...
  after_create_commit -> { 
    broadcast_render_to(
      :notifications,
      partial: "notifications/new"
    )
  }
  ...
end

Selective broadcasting?

If you want to multicast a turbo stream instead of broadcasting it, just make sure it reaches the right receivers. You can distinguish between your receivers by giving them their own channels.

For an example, different users are in different groups, and you want only the people inside the group to get the updates, how do you distinguish them from all the other users in the other groups?

<%= turbo_stream_from(@group) %>

This will create a unique channel for each group, and when you broadcast your updates,

# app/models/Post.rb

class Post < ApplicationRecord
  belongs_to :group
  ...
  after_create_commit -> { 
    broadcast_render_to(
      group,
      partial: "notifications/new"
    )
  ...
end

It will be broadcasted to only the required groups/channels.

Testing your broadcasts

Of course, you would want to make sure your broadcasts are going through the correct channels but how do you test that? With ActionCable test helpers.

test "notifications are sent to same group" do
    stream = @group
    target = "icon"

    assert_broadcast_on stream, turbo_stream_action_tag("replace", target: target, template: %(<span class='icon'>notification_new</span>)) do
      Post.create(title: 'A new post')
    end
end

High response time!

If the broadcasting of your turbo stream is an expensive operation, just create a Turbo::Streams::BroadcastJob to broadcast later in a background job. This is done using broadcast_*_later_to methods.

# app/models/Post.rb

class Post < ApplicationRecord
  belongs_to :group
  ...
  after_create_commit -> { 
    broadcast_render_later_to(
      group,
      partial: "notifications/new"
    )
  ...
end

You can try these examples in the demonstration app here. That's all about broadcasting for now. Until next time.