Action Cable is a framework to allow multiple clients to receive seamless, simultaneous updates from a Rails server. If you have an application where changes submitted by one user should be seen immediately by all users without a refresh, Action Cable might be the solution you’re looking for.
In my case, I was developing a two to four player variant of Chess where all players needed to be able to see every move made by the other players. Since all players are being served by the same server, they could see the new state of the board by refreshing their page, but that is clearly not an ideal way to handle the issue.
Action Cable integrates WebSockets into an application. WebSockets are a way to establish a real time connection between the client and the server where changes on the server-side are reflected on the client-side without any initiating action on the part of the client.
installation
If you’re using the newest version of Rails, it comes loaded with Action Cable from the start, so there’s nothing you need to do on the server-side initially.
In my case, I was building the front end with JS/React. To install action cable for a JavaScript app run from the command line:
npm install actioncable --save
server-side setup
In your rails app you’ll find a directory called “Channels”. This will be where you’ll do the server-side setup for Action Cable.
First, add a directory within /Channels called “application_cable”. Move the file “channel.rb” into that directory. After that, you can leave that file alone.
Second, create a new file in /Channels to handle the specific connections you want to server. You can think of a channel as a particular stream transmitting selected data from the server to the client. Many clients can subscribe to the same channel and receiver that data-stream. You can also set up many channels to control which data gets sent when. For this demonstration, we’ll just set up one channel, which I called “game_channel.rb”.
Inside this file, you’ll set up your specific channel:
class GameChannel < ApplicationCable::Channel
def subscribed
stream_from "game#{params[:id]}"
game = Game.find(params[:id])
ActionCable.server.broadcast("game#{game.id}", game)
end
end
Let’s break this down a bit. This file defines the class GameChannel, which inherits from the Channel class in the ApplicationCable module, which in turn inherits from the ActionCable channel base.
In this class we have created the instance method “subscribed”. This dictates what should happen when a client subscribes to this channel. First, the name of the particular subscription is established. In this case, it is “game” followed by the id number of the game, e.g., “game3”. This is how this stream will be referenced in other parts of the app.
Next, we indicate what should be done when someone subscribes to this channel. In this case, the relevant game is found and broadcast on the relevant channel. This data will be received on the client-side and handled from there.
But we don’t want to send data just when a user subscribes to the channel. We also want to send it whenever anyone makes a move. To do this, we need to include broadcaster inside the relevant controller, in this case, the BoardsController:
def move_piece
board = Board.find(params[:board_id])
game = board.game
players = game.players
if players.map{|player| player.user_id.to_i}.include?(session[:user_id])
legal = board.move_piece(params[:start_loc], params[:end_loc], session[:user_id])
if legal
board.fill_camp
game.advance
package = game.package
ActionCable.server.broadcast("game#{game.id}", package)
render json: package, status: :accepted
else
return render json: { error: "Illegal move." }, status: :not_acceptable
end
else
return render json: { error: "Not authorized" }, status: :unauthorized
end
end
There’s a fair amount going on here that’s specific to this game. The important thing to notice is that, once authentication and validation is complete, the same data that is rendered to the client who made this request is broadcast to any client that is subscribed to the relevant game channel.
Anywhere you want to broadcast server-side changes to all subscribers, simply include ActionCable.server.broadcast("channel-name", data).
The last thing you need to do on the server side is establish a route for subscription requests:
#actioncable routing
mount ActionCable.server => '/cable'
client-side setup
In your React app, create a directory called “Channels” within /src. Inside that directory, you’ll need to create the file “index.js”:
import * as ActionCable from '@rails/actioncable'
window.App || (window.App = {});
window.App.cable = ActionCable.createConsumer();
Next, determine what component of your app is best suited to handle the subscription. For my app, it is the Games component. Whenever a player selects a game, they subscribe to the channel for that game:
import { createConsumer } from "@rails/actioncable"
...
function Games({}) {
const [cable, setCable] = useState(createConsumer('ws://localhost:3000/cable'));
const [subscript, setSubscript] = useState({});
function subscribe() {
const sub = cable.subscriptions.create({
channel: 'GameChannel',
id: selectedGame
}, {
connected: () => {
const identifier = JSON.parse(sub.identifier);
console.log(`Connected to the channel: GameChannel ${identifier.id}`);
},
disconnected: () => {
const identifier = JSON.parse(sub.identifier);
console.log(`Disconnected from the channel: GameChannel ${identifier.id}`);
},
received: async (data) => {
const identifier = JSON.parse(sub.identifier);
console.log(`Receiving data from channel: GameChannel ${identifier.id}`);
setGamePkg(data);
}
});
setSubscript(sub);
};
useEffect(() => {
cable.subscriptions.remove(subscript);
if (selectedGame !== "none") {
subscribe();
}
}, [selectedGame]);
};
The subscribe function handles the creation of the subscription, reports whether the subscription was successful, and returns the broadcast data if it was successful.
Note that the address parameter for createConsumer should be whatever the address is for your Rails server.
Note also that when a subscription is created, it is stored in state. This state is then used to remove the subscription whenever a new game is selected.
Note also that the data delivered via the subscription is stored in the gamePkg state. It is then ready to be used in whatever way you see fit.
redis
You’ve almost finished setup. The last thing you need is to install and configure Redis. Redis is a data storage tool that acts as the middleman data cache between the the server and the client. Without Redis, Action Cable will not function.
To install Redis on your Rails app, run from the command line:
sudo apt-get install redis
To run the Redis server (which will need to be running while you use the app), run from the command line:
redis-server
Finally, check to make sure that your /config/cable.yml file is configured properly:
development:
adapter: redis
url: redis://127.0.0.1:6379
test:
adapter: test
production:
adapter: redis
url: redis://127.0.0.1:6379
channel_prefix: tunnel_brawl_production
My app is called “Tunnel Brawl”. You should name your channel_prefix appropriately.
conclusion
With the Redis server running, the Rails app running, and the React app running, you should be able to subscribe to Action Cable channels and get live updates whenever the relevant data on the server changes.
There’s a lot more that you can do with Action Cable and WebSockets, but I hope this is enough to get you up and running.
