I am currently using Action Cable in order to implement a live draft for my fantasy sumo wrestling app. There are a few articles on the topic already which I’ve found very useful and will link to at the end of this blog. I want to provide another resource for someone trying to use Action Cable for an app that uses Ruby on Rails for the backend and React for the frontend in which you want to show who’s connected to Action Cable on every client.
Don’t change the channel, let’s get started!
What is Action Cable?
Action Cable is a built-in Rails library that facilitates WebSocket integration. Action Cable allows you “access to your full domain model written with Active Record or your ORM of choice.” It’s worth talking a little bit about what WebSockets are.
WebSocket is a protocol for communication, usually between a server and one or many clients. Unlike HTTP, a WebSocket connection allows for the transfer of data to and from the server, also known as “full-duplex” communication. To the left is the process of creating a WebSocket connection.
The “handshake” uses the HTTP Upgrade header to change the HTTP protocol to the WebSocket protocol. Once this is accepted data can be sent back and forth between the client and server. The server can even broadcast messages and data unprompted from the clients.
What we have so far is that Action Cable is a library that comes with Rails to utilize WebSockets, or WebSocket protocol, which allows for the transfer of data in a free-flowing, back and forth style (full-duplex communication) unlike the usual HTTP request/response cycle. Right on.
Setting up Action Cable
With a very basic understanding of what Action Cable is I will now walk through setting it up. Since I have been learning about Action Cable and using it in my fantasy sumo app that is the example I will use to explain.
To set up Action Cable in Rails on your backend, there are 4 things you must do:
- Create your channels. I used
rails g channel leaguebecause I’m down with Rails generators and I want a channel for each league. This creates a LeaguesChannel file in our channels folder.
- In your routes file add
mount ActionCable.server => '/cable'. This adds the Action Cable endpoint. You can name the
'/cable'bit whatever you want really.
- In your gemfile uncomment out the ‘redis’ gem. Redis is an ‘in-memory data structure store’ that Action Cable uses to keep track of stuff. Like a quick access database just for your WebSockets.
- In your config folder, open up cable.yml and change the development adapter from
redisand underneath that add a url property and provide it with the same url that’s in the production settings underneath. I have heard that it doesn’t work very well in development mode when the adapter is set to
async, it is beyond me why so I followed the ubiquitous advice to do this step. Here is what your cable.yml should look like:
Configuring the frontend is pretty simple. Here are the steps:
npm install actioncable --save. This provides us with the node package for Action Cable that includes methods to connect to our WebSocket in the backend.
- Add ‘actioncable’ to your index.js file and invoke the ‘createConsumer’ method included in the node package like so:
We are importing actionCable from the package we just installed. Then we are creating a CableApp object to store all of our cable related items. We create a property in that object called
cable and set it equal to the invocation of that createConsumer method which connects to the address that we specified on the backend. Then we can pass our CableApp object down to our App component.
Subscribing to Action Cable
Now that everything is set up we can put in some code so that our frontend is subscribed to a channel on our backend and receive broadcasts. In my app I want the subscription to occur when a user opens a draft for a league. So I passed the CableApp object down to my Draft component and subscribe in a useEffect on the component.
We create a new property of the CableApp object called “league” and set it equal to a function, subscriptions.create, that we call from the cable property of CableApp. This function takes two arguments: the first one specifies which channel to subscribe to and includes whatever parameters we give it and an object containing some lifecycle functions specific to an Action Cable subscription. In this example I am connecting to the “LeaguesChannel” on my backend and pass it the league id as well as the current user’s id which are both kept in my frontend’s store.
At the bottom of my useEffect I return an anonymous function which I use to clean up (useEffect can return a function to take care of side effects). Since I don’t want a user to still be subscribed to the channel when they leave the draft page, I call the disconnect method on cable within the CableApp object.
Now let’s look at the channel we’re subscribing to on the backend. This is one of the files we created when we called the rails generator at the beginning of this article.
A couple of methods are already provided after we generate a new channel. I have already added some code here to my subscribed method. This method uses the league id and user id that we provided on the frontend, finds those records in our database and creates a stream for that specific league record.
This channel I created from “ApplicationCable::Channel”. With Action Cable we have access to the general Channel class as well as a Connection class. These are very useful for authorizing and authenticating users, especially in a pure Rails application. Since I am authorizing and authenticating on the frontend and feel confident that an invalid user would not be able to access the draft page I have not done this.
Once we mount and render our draft component from the browser we should be subscribed and our backend’s logs should say something like…
Showing a User is “Online”
Let’s add some functionality so that whenever a user is subscribed to the channel an icon next to their name will turn from grey (inactive) to green (active). First let’s add some code to our frontend.
These connected and received functions are inside of my subscription creation function which is inside of the draft component’s useEffect. Line 3 in connected is telling the subscribed instance of LeaguesChannel on the backend to invoke an “appear” function which we’ll create in a moment. Received now takes the blandly labelled “data” as a parameter and gives that to updateOnlineStatus.
This function finds a CSS class on the page ‘user-<userid>-status’. Every user card on the draft page is rendered with a little • icon next to it and assigned to this class like this…
Whenever the frontend receives a broadcast that a user is online the received function will send that user’s id to the updateOnlineStatus function so that that icon can also be given the class “online”. In the CSS this class changes the color to a vibrant green so that the user’s card now looks like this…
Let’s go back to the
connected() function on our frontend and what
Since the connected function gets defined inside of the creation of our subscription (inside of the useEffect, refer to the draft component above),
this refers to
props.cableApp.league. When we call
this.perform('appear') we are telling the LeaguesChannel that there’s a function called “appear” that we want to call. So let’s do that!
I’ve updated the LeaguesChannel file to include an “appear” method. It will broadcast an object to the channel which contains the current user’s id and a status of “online”. I have also updated the unsubscribed method to broadcast the same thing except with the status “offline”.
So now when we receive a broadcast on this channel and we send the data received to our updateOnlineStatus function, we will have a user id to match to the user status icon on the DOM and a status of “online” or “offline” to let us know if the icon should have the “online” class thus giving it a green icon.
Next Challenge: Showing Already Online Users
An issue that I quickly ran into was that when I opened a new browser window and logged into another account in a draft, both users would appear online for the first user but the user that was originally online would appear offline to the latest joining user.
I tried querying the current subscriptions to the channel but that proved to be more complicated than I thought (the Action Cable connection objects can be pretty big and verbose). Then I thought I could just keep a reference to every connected user somewhere, maybe the Connection class. But it doesn’t really work like that either — besides, we’d have to iterate over this collection of connected users every time and that seemed not great.
I stumbled upon a lukewarm disagreement on Github that roughly concerned this and the solution provided was to disconnect all of the connections when a new connection was made, forcing a reconnection and then all subscribers have the most up to date information. Although it didn’t feel very elegant I settled on this paradigm for the time being. I played around with it a little bit and after a lot of endlessly disconnecting and reconnecting in loops I found a solution that is working.
In LeaguesChannel I added this method for “reappear”. Ideally I would just pass an argument to my “appear” method but I couldn’t find documentation on passing arguments on the frontend when using the
On the frontend in my “received” function I provide some conditions and if they’re all met then the “reappear” method is called and newer users can see users that were already online when they subscribe to the channel.