Back to home

Action Cable Hello World With Rails 7 main image

Action Cable Hello World With Rails 7

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.

Source code can be found here

Prerequisites

  1. Basic familiarity with setting up a new Rails project.

Getting started

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:

<!-- app/views/rooms/index.html.erb --> <h1>Rooms#index</h1> <div id="root"></div> <!-- app/views/rooms/show.html.erb --> <h1>Rooms#show</h1> <div id="root"></div>

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

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:

import * as React from "react"; import * as ReactDOM from "react-dom"; import consumer from "../channels/consumer"; // interface WebSocketProps {} function WebSocket() { const [messages, setMessages] = React.useState([]); React.useEffect(() => { consumer.subscriptions.create( { channel: "RoomChannel" }, { received(data) { setMessages([...messages, data.message]); }, } ); }, [messages, setMessages]); return ( <div> {messages.map((message: string, index: number) => ( <p key={`message-${index}`}>{message}</p> ))} </div> ); } document.addEventListener("DOMContentLoaded", () => { const rootEl = document.getElementById("root"); ReactDOM.render(<WebSocket />, rootEl); });

Our updated component now does the following:

  1. Creates a new subscription to the RoomChannel channel.
  2. Receives messages from the channel and updates the state.
  3. 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:

irb(main):001:0> ActionCable.server.broadcast 'room_channel', message: 'hello' => 1 irb(main):002:0> ActionCable.server.broadcast 'room_channel', message: 'world' => 1

If we view our browser, we will now see the messages displayed!

Received messages on the frontend

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:

import * as React from "react"; import * as ReactDOM from "react-dom"; import { BrowserRouter, Routes, Route, useParams } from "react-router-dom"; import consumer from "../channels/consumer"; function WebSocket() { const [messages, setMessages] = React.useState([]); const params = useParams(); React.useEffect(() => { consumer.subscriptions.create( { channel: "RoomChannel", room_id: params.room_id ?? undefined }, { received(data) { setMessages([...messages, data.message]); }, } ); }, [messages, setMessages, params]); return ( <div> {messages.map((message: string, index: number) => ( <p key={`message-${index}`}>{message}</p> ))} </div> ); } function NotFound() { return <p>Not Found</p>; } document.addEventListener("DOMContentLoaded", () => { const rootEl = document.getElementById("root"); ReactDOM.render( <BrowserRouter> <Routes> <Route path="rooms" element={<WebSocket />}> <Route path=":room_id" element={<WebSocket />} /> </Route> <Route path="*" element={<NotFound />} /> </Routes> </BrowserRouter>, rootEl ); });

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):

  1. http://localhost:3000/rooms/123
  2. http://localhost:3000/rooms/123
  3. http://localhost:3000/rooms
  4. 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:

irb(main):027:0> ActionCable.server.broadcast 'room_channel_123', messa ge: 'hello room 123' [ActionCable] Broadcasting to room_channel_123: {:message=>"hello room 123"} => 1

Broadcasting to room_channel_123

Broadcasting to room_channel_123

Success! Now we can test the /rooms channel by broadcasting like the following:

irb(main):028:0> ActionCable.server.broadcast 'room_channel', message: 'hello room' [ActionCable] Broadcasting to room_channel: {:message=>"hello room"}

Broadcasting to room_channel

Broadcasting to room_channel

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.

Resources and further reading

Photo credit: andrewchildress

Personal image

Dennis O'Keeffe

@dennisokeeffe92
  • Melbourne, Australia

Hi, I am a professional Software Engineer. Formerly of Culture Amp, UsabilityHub, Present Company and NightGuru.
I am currently working on workingoutloud.dev, Den Dribbles and LandPad .

1,200+ PEOPLE ALREADY JOINED ❤️️

Get fresh posts + news direct to your inbox.

No spam. We only send you relevant content.