Rails 6: Subscribing to Multiple Channels in Action Cable
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.roomRoomChannel.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:
- Look for an
id
in the url - 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!