# Action Cable Hello World With Rails 7

> Dive head-first into using Action Cable with Rails 7 and setup a basic React project that can handle web sockets.

- Canonical: https://dennisokeeffe.com/blog/2022-02-28-action-cable-hello-world-with-rails-7
- Published: 2022-02-28
- Updated: 2022-02-28
- Tags: ruby, ruby-on-rails

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](https://github.com/okeeffed/demo-action-cable-hello-world)

## Prerequisites

1. Basic familiarity with setting up a [new Rails project](https://guides.rubyonrails.org/getting_started.html).

## Getting started

We will use Rails to initialize the project `demo-action-cable-hello-world`:

```s
# 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:

```rb
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`:

```html
<!-- 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](https://blog.dennisokeeffe.com/blog/2022-02-19-rails-7-using-react-with-esbuild) 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:

```ts
// 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.

```tsx
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](https://cdn.dennisokeeffe.com/assets/2022-02-28-action-cable-hello-world-with-rails-7/1-running-app.webp)

## 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`:

```s
# 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:

```rb
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:

```rb
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:

```js
// 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:

```tsx
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:

```s
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](https://cdn.dennisokeeffe.com/assets/2022-02-28-action-cable-hello-world-with-rails-7/2-chat-running.webp)

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

```tsx
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 `<NotFound />` 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:

```rb
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:

```s
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](https://cdn.dennisokeeffe.com/assets/2022-02-28-action-cable-hello-world-with-rails-7/3-room-123-only.webp)

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

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

![Broadcasting to room_channel](https://cdn.dennisokeeffe.com/assets/2022-02-28-action-cable-hello-world-with-rails-7/4-room-channel-only.webp)

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

- [Rails getting started](https://guides.rubyonrails.org/getting_started.html)
- [Source code](https://github.com/okeeffed/demo-action-cable-hello-world)

_Photo credit: [`andrewchildress`](https://unsplash.com/@andrewchildress)_
