Learn Action Cable and React by building a real-time messaging application

Kody Samaroo
8 min readAug 15, 2021

I spent many hours trying to connect subscribers with Action Cable, a technology in Rails that is capable of integrating Websockets in an application. Websockets are a TCP protocol like HTTP is a protocol but is made with the intention of sending data to and from multiple clients. In order to illustrate and provide the best introduction into the topic I will demonstrate how to build a real-time messaging application with this stack. It will not be extremely detailed and the code may not be the cleanest but it is designed to give you a basic idea of how to use these technologies. I will also deploy this project with Heroku and Netlify

Tech-Stack

// Backend
Ruby 2.6.6
Rails 6.1.3
ActionCable 6.1.3
Postgres
Redis
Rack-Cors
// Frontend
React (Anything could work but I enjoy the organizational aspect of components and hooks)
ActionCable
// Deploy Services
GitHub
Heroku
- Redistogo
Netlify

Rails setup

We will do a quick setup for the backend and much of the other sections of this project. Make a directory with mkdir <project_name> or the GUI way, right-click and “new folder”. cd into the directory and run the IDE of your choice, in VSCode the command is code ..

Rails new <project_name_api> --database=postgresql

A postgres db will save you time when its time to deploy to Heroku so for me its recommended, but feel free to use what is comfortable for you. A link and documentation to download postgres is here, it can be tricky but is worth it in my opinion.

Next we will create the models, controllers and channel for the action cable. This is a lot of repetitive code but here we go. Learn about Rails MVC here.

// Users
rails generate model User name:string
rails generate controller User
// Rooms
rails generate model Room title:string
rails generate controller Room
// Messages
rails generate model Message content:string
rails generate controller Message
// Channels
rails generate channel Room
// Associations
rails generate migration AddUserToMessages user:references
rails generate migration AddRoomToMessages room:references
rails db:migrate

These commands should generate a schema similar to this

Messages will have a foreign key of the room_id and user_id. With this foreign key we can reference the room a message was sent to as well as the user that sent the message. We can further take advantage of this association in serializers to access attributes related to the association, but more on this later.

We will also had in the following associations in the models.

Associations

Next we will setup the Action Cable. There should be a ‘subscribe’ method and an ‘unsubscribe’ method. These methods represent when a client connects or disconnects to a channel and are necessary to keep.

For the subscribe method you could hard code the channel in like the comment says and this may work for a one room chat but we will program something a little more dynamic and scalable.

Routes

Next we will get the routes setup for the backend. We will take advantage of ‘resources’ to handle the standard routes but we will also add one more custom route to connect to the Action Cable.

Cors

Last thing just to make sure domains are properly accepting requests and sending responses. We will use rack-cors to handle this.

gem 'rack-cors' // Add this gem to the Gemfile
bundle install // Run this command in the console to install

From the documentation we get a sample of what we can use to handle cors. An ‘*’ like suggested is perfectly acceptable in the production environment, and makes things a lot easier. It is good practice to get in the habit of using an array to pass acceptable origins.

I use http://localhost:3000 because this is will be the frontend url. Also not it is the http protocol not to be confused with the secure https that is usually seen in URLs.

Lastly, adding some seed data to test, later we can generate Users, authenticate with a login or what have you.

rails db:seed

React

Now we can setup up the frontend and actually start sending requests and getting feedback. We will quickly build this out so barring some crude styling our functionality will be perfect.

cd .. // Go back to the root project directory
npx create-react-app client

Note: I prefer to keep everything related to the project in one directory but when it comes to making Git repos I believe it’s better to separate the two. Our hierarchy tree should look like this.

chatapp
--chatapp_api
// Rails backend stuff
--client
// React frontend stuff

I also like to scrape the boilerplate code that React builds. For all the great things React does with "create-react-app”, the bulky boilerplate code is something I wish was omitted, but its quick and gets the job done so we will work with it.

App.js will have a div to hold messages, the H1 is a placeholder. The input and the button.

States and Hooks

The output of all messages needs to set in State so that it can updated as messages are created and sent. The useState hook is declared and can be used to declare stateful getters and setters. We want to go through the array of messages and return some JSX code that will display the messages.

State variables (line 7 & 8) — Declaring state variables, “messages” will contain a collection of messages while “message” will be the current message that gets sent to the handle. Then we will console.log the response that comes from this request, to see what exactly are we looking at. Also notice that the value of the input is set to the ‘getter’ for ‘message’ and there is an onChange effect that continuously updates the state. This is known as a controlled input.

Handler (line 11) — The function that handles when the send button is pressed. We are using a fetch here, there are many ways to utilize the fetch API in JavaScript here is a resource for that. It is a ‘post’ request so the ‘message’ can be sent to the backend with JSON.stringify and added to the database. The button is given an onClick event that will trigger the handler.

JSX (line 25) — The JSX is much the same with added classNames for elements to target in the CSS as well as some event listeners to round everything out.

Controller Action

When we follow the send message handler from the frontend to the backend we create a controller action ‘create’. This route is identified as …

post 'create', to: 'message#create'

The controller action simply takes the message and creates a record in the database. Because the user and room associations those foreign keys are necessary for the record creation. The default user and room have an id of 1, so simply hardcode it for now just to test outcomes.

The second part of this action is the ‘broadcast’. The ‘if’ keyword checks to see if the record was created and broadcasts the record to that room. The broadcast_to takes two parameters, the room that will be broadcasted to and an object. The attributes being broadcast can be specified here and they will carry over into the frontend as an object.

Action Cable

The Action cable connection will look something like this, again with a hardcoded title for the room. First set ‘connection’ to an empty object otherwise you won’t be able to add the .cable method to the variable.

let connection = {}

createConsumer is a method given by actionCable and it takes a route as a parameter. Note that ws is used instead of http it is a protocol used for TCP. /cable matches the mount route defined in routes.rb. The useEffect is creating subscriptions whenever a consumer is created and connects the subscriber to the proper channel in this case it is the RoomChannel, the file that is in our backend. General is hardcoded as the title this could be dynamic by passing a variable as the room title name.

The create object also has a parameter for an object of methods that are linked to the RoomChannel from our Rails backend. ‘Connected’ will always be subscribed, this will be were a connection status would be something like an admin message to the chatroom “User has joined the chat”. However a simple console.log will be enough proof to show whether the connections are working.

‘Received’ is what happens after the message record was created from our backend. If this connection is functioning an object of the created record should show up in the log.

Showing messages

Once everything is showing in the console as it should, next step is to update the state, but also preserving the previous state. This line of code basically appends the newest message to the array of messages. By keeping track of the messages in this manner all of the users can add to the array as long as they consume the subscription connected to the room channel.

function updateMessages(data) {
setMessages([...messages, data])
}

This function, “updatedMessage” uses the spread operator to append the “data” to the messages state. The parameter for this function comes from the received function in our action cable consumer. The flow of data is as follows:

User sends message > The message is saved in the backend > Action Cable broadcast the message to the channel > subscribers of the channel receive and consume the data in the client > The message is displayed in JSX.

The last step to display the message is simple. Map through the messages pulling out each individual message and index. Then return a JSX out using the elements to display the ‘message.message’ and the ‘message.user’. Don’t forget to use the index as the key for each message because React requires that children have unique keys.

const displayMessages = messages.map((message, index) => {return (<div key={index} className="message">
<div className="text"> {message.message} </div>
<div className="author"> {message.user} </div>
</div>
)
})

Future

This concludes the tutorial later on we will deploy this application using Heroku and Netlify.

--

--