This post will show you how to use Action Cable to create a simple web socket application with React.js that has independent, public chat rooms that you can subscribe to.
We will use Rails to initialize the project demo-action-cable-hello-world:
# Create a new rails project
$ rails new demo-action-cable-hello-world -j esbuild
$ cd demo-action-cable-hello-world
# Create the required controllers
$ bin/rails g controller rooms index show
# Installed required packages
$ yarn add react react-dom @rails/actioncable react-router-dom
# Start the server
$ bin/dev
Updates routes.rb for your resources:
Rails.application.routes.draw do
resources :rooms, only: [:index, :show]
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
# Defines the root path route ("/")
root "rooms#index"
end
We also need to update the generated erb files for rooms:
We will be attaching our React app to the root element. You can read more about this on my blog post that sets up a new Rails app with React and ESBuild.
Update the app/javascript/application.js to app/javascript/application.ts and add the following:
// Entry point for the build script in your package.json
import "@hotwired/turbo-rails";
import "./components/WebSocket";
Finally, let's update the code to reference our WebSocket component that we created earlier. This will only be a basic scaffold for now.
import * as React from "react";
import * as ReactDOM from "react-dom";
interface WebSocketProps {
arg: string;
}
function WebSocket({ arg }: WebSocketProps) {
return <div>{`Hello, ${arg}!`}</div>;
}
document.addEventListener("DOMContentLoaded", () => {
const rootEl = document.getElementById("root");
ReactDOM.render(<WebSocket arg="Rails 7 with ESBuild" />, rootEl);
});
At this stage, if we run bin/dev to start up both the Rails server and ESBuild, we can navigate to http://localhost:3000/rooms and http://localhost:3000/rooms/whatever and see our React app running:
React app image
Generating our channel
We can use bin/rails g channel to generate a new channel. We will use this to create a new channel called RoomsChannel:
# Scaffold the channel files for a room channel
$ bin/rails g channel room speak
A file app/channels/room_channel.rb has been added, which we will add the following:
class RoomChannel < ApplicationCable::Channel
def subscribed
stream_from 'room_channel'
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def speak
ActionCable.server.broadcast 'room_channel', message: data['message']
end
end
The speak method will be called when a message is sent to the channel.
Next, we want to mount an Action Cable server for our Rails application. Update config/routes.rb to add the following:
Rails.application.routes.draw do
resources :rooms, only: %i[index show]
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
# Add in our Websocket route
mount ActionCable.server => '/cable'
# Defines the root path route ("/")
root 'rooms#index'
end
Now we will have a cable web socket setup at /cable.
At this stage, we can update our frontend code to connect to the web socket server.
Receiving broadcasts on our frontend application
Inside the file app/javascript/channels/consumer.js, ensure it looks like the following:
// Action Cable provides the framework to deal with WebSockets in Rails.
// You can generate new channels where WebSocket features live using the `bin/rails generate channel` command.
import { createConsumer } from "@rails/actioncable";
export default createConsumer("http://localhost:3000/cable");
Here we are using the createConsumer helper to create a new consumer for our Rails application. We will use this consumer to connect to the web socket server that we created at route /cable.
In order to begin receiving messages on the frontend, let's update our WebSocket.tsx component to make use of the consumer:
Creates a new subscription to the RoomChannel channel.
Receives messages from the channel and updates the state.
Renders the messages received from the channel.
This will only maintain local state and not send any messages to the server. For now, reloading the page would reset the state.
This is fine for now, as we are only looking to displaying broadcast messages on the frontend.
Note: This is a contrived component. Usage of the index for the key is not recommended and we will be looking at using global state to help manage any messages that are received on the frontend in the next post.
If we now head to the page http://localhost:3000/rooms, we can start sending broadcast messages.
Sending messages to the channel
At this stage, we can send messages to the channel using the Rails console to test things out.
Run the Rails console with bin/rails console and type the following:
If we view our browser, we will now see the messages displayed!
Received messages on the frontend
Adding a room
At the moment, if we also head to http://localhost:3000/whatever, we can still see any messages sent to the channel. What happens if we want to only display messages sent to a particular room that marries up to the URL?
We can do so by setting up our component to subscribe to a particular room and broadcasting to that room.
First of all, we need to make use of react-router-dom to help us out. Although this router example will be contrived, it can convey what we are trying to do. Update the WebSocket.tsx component to the following:
In this case, we are enabling /rooms and /rooms/:room_id to return the <WebSocket /> component while the fallback is a basic `
component.
We now also use the useParams hook to get the room_id from the URL and set that for the first consumer.subscriptions.create argument. If there is no params.room_id, then we set undefined.
This behavior will effectively attach us to rooms_channel only if we are on /rooms and rooms_channel_<room_id> if we are on any /rooms/:room_id where the ID will match up to the URL.
At this point, we need to update our file `` to reflect this behavior:
class RoomChannel < ApplicationCable::Channel
def subscribed
if params[:room_id]
stream_from "room_channel_#{params[:room_id]}" if params[:room_id]
else
stream_from 'room_channel'
end
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def speak
puts params
if params[:room_id]
ActionCable.server.broadcast "room_channel_#{params[:room_id]}", message: data['message']
else
ActionCable.server.broadcast 'room_channel', message: data['message']
end
end
end
We are now ready to test out this behavior!
Broadcast messages to individual rooms
At this point, I've now opened up four individual Firefox windows to test out our channels with the setup as follows (going clockwise from top-left):
http://localhost:3000/rooms/123
http://localhost:3000/rooms/123
http://localhost:3000/rooms
http://localhost:3000/rooms/1234
When we start to broadcast our messages, we will expect rooms (1) and (2) to see messages broadcast to room_channel_123, (3) to see messages broadcast to room_channel and (4) to see messages broadcast to room_channel_1234. We will only test the first two rooms.
In the Rails console, send the first message like so:
Awesome success. We can now see that we are sending to different channels on our frontend based on the routing.
Summary
Today's post demonstrated how to set up a basic Rails application with Action Cable and React.
This post also demonstrated how to send messages to individual rooms. Other things you could do from here is enforce that rooms are not public and can support private rooms.
In the next post, I will be implementing Redux to demonstrate how we can use Redux to manage our message state and run different actions.