Rails 6: Subscribing to Multiple Channels in Action Cable

Jelani Woods
3 min readJul 9, 2019

--

There are a lot of tutorials about making a basic chat application with Action Cable, and most of them create an app with one general Channel that acts as one general chat room that all users subscribe to automatically. Maybe you have a model like Room or Topic and users only need to be subscribed for an instance of the that model.

So the question is, how can stream updates to specific channel that’s based off a database model and how can users subscribe to these specific channels?

Streaming from a specific resource

This part is easy!

When there was only one Channel you would set the subscription process like so:

# In room_channel.rbdef subscribed
stream_from "room_channel"
end

broadcast to it like

# In messages_controller#create
ActionCable.server.broadcast "room_channel",
body: @message.body

Now when you broadcast your message, you can broadcast from a specific Channel, the RoomChannel. What’s even better, you can broadcast to a specific instance of a Room.

# In messages_controller#create
room = @message.room
RoomChannel.broadcast_to room,
body: @message.body,
user: @message.user.name

Next we need to modify our room_channel.rb to stream for specific Room objects.

stream_for roomdef room
Room.find(params[:room_id])
end

stream_for works similarly as stream_from with the difference that stream_for accepts an ActiveRecord and will automatically name the Channel based on the record’s grid param id.

But what is params here? and how can we add keys and values to it?

Most importantly — How do we set the room_id?

Subscribing to a Specific Resource

Now we just need to subscribe to a specific Room, but how do can we find out which Room to subscribe to?

We can rely on some JavaScript to get the job done.

In app/javascripts/channels/room_channel.js , we have something like:

consumer.subscriptions.create("RoomChannel", {
connected() {
...
},
disconnected() {
...
},
received(data) {
...
}
});

Here ,where the subscription is created, we can specify params for the RoomChannel ruby class.

So just to make sure this works, let’s say we have a Room with id of 1 and the route looks something like /rooms/1. Let’s create a subscription to that specific Room.

We can modify our JavaScript to pass a room_id like so:

consumer.subscriptions.create({
channel: "RoomChannel",
room_id: 1
}, {
...

Now if you start your server and visit a page, we should see something in the server log like

RoomChannel is streaming from room:Z2lkOi8vYWN0aW9uLWNhYmxlLXJhaWxzNi9SZXNvdXJjZS8x

Great! We got our clients to connect to a Channel's specific Room.

This seems kinda dumb though, because it’s functionally the exact same as what we were doing before. It’s actually worse, because all clients are only subscribed to "RoomChannel" with room_id: 1, while they could potentially send messages to "RoomChannel" with any room_id. If they were to do that, they would need to refresh the page to see any new messages.

So, how do we make this room_id dynamic?

Ya got two options:

  1. Look for an id in the url
  2. Look for an id embedded in the page

I prefer the embedding the id in the page. So I modified my Room show page by adding a data-room-id attribute to the containing div and set the value to the id of the current Room.

<div class="col-md-8" 
id="room_messages"
data-channel-subscribe="room"
data-room-id="<%= @room.id %>">
...
</div>

Now in room_channel.js I can modify the subscription process to grab the id from the page

consumer.subscriptions.create({
channel: "RoomChannel",
room_id: $('#room_messages).attr('data-room-id')
}, {

BUT WAIT!

This won’t actually work yet because this JavaScript happens before the html finishes loading. This means data-room-id isn’t set yet and won’t have the id value that we’re expecting!

How do we fix this? Wait for turbolinks to load first.

$(document).on('turbolinks:load', function () {
consumer.subscriptions.create({
...
})

— And just like that we made it work! Depending on what Room show page we’re on, the client will subscribe to a specific Room channel and send messages only to that channel. The client will also see updates to that channel without having to refresh the page.

I made an example repo with working code on branch jw-multiple-channels if you want to check it out!

--

--