Using Action Cable With React
For a recent project, I made a chat app with a React front-end and a Rails API back-end with the goal of familiarizing myself with WebSockets, and Rails’ tool for implementing them, Action Cable. Unfortunately, this turned out to be more difficult than expected — while there are no shortage of tutorials on Action Cable, almost all of them presume the reader to be working within a full stack Rails environment. The few that I found which purported to show how to integrate Action Cable with React either integrated React into Rails directly (using the react-rails or react_on_rails gems) or were so convoluted or reliant on extraneous libraries that it was difficult to follow along.
I felt I had stumbled upon a “hole in the internet”, so to speak… unable to find any tutorials which related directly to what I was attempting to do, I had to piece my solution together from a variety of different sources. I hope that, in writing this, I might be able to help make the process easier for those who follow.
Before we start, I should note that I’m running Rails 5.1.4 and React 16.2. The finished code for this tutorial is available here (backend) and here (frontend), and you can check out the finished product on Heroku. Let’s get started!
What Are WebSockets?
WebSockets are a protocol, like HTTP. But the difference between WebSockets and HTTP is that WebSockets maintain a persistent connection between the client and server.
Consider how HTTP works in practice. The user inputs a URL in their browser (the client), which sends a certain kind of request (‘GET’, ‘POST’, etc.) to the machine hosting the website (the server). The server receives this request, prepares some text (usually HTML or JSON), and sends this text as a response back to the client, which receives and displays the response.
This kind of communication is referred to as half-duplex. Let’s break that down:
- Duplex refers to the ability for two points or devices to communicate with each other bi-directionally. This is as opposed to Simplex systems, which can only communicate unidirectionally.
- Half-duplex means that when the two points or devices communicate, one must be the transmitter and one must be the receiver—The two cannot transmit or receive simultaneously. When one is sending, the other is receiving, and vice versa.
So basically, between the client and server, only one side can be “talking” at a time. You can think of this as akin to how walkie-talkies work… One person presses the button to talk, and when they’re finished they say “over” (the equivalent of an HTTP status code) and release the button, whereupon the other person is free to respond.
By contrast, WebSockets, introduced in 2009, allow clients and servers to maintain full-duplex connections. If half-duplex connections are like walkie-talkies, full-duplex connections are like cell phones — both sides can receive and transmit at once. In practice, the client makes an initial request to upgrade the connection into a WebSocket connection, upon which the server, if it accepts (perhaps after authentication), gains the ability to push updates to the client. This is incredibly useful for real-time web applications, such as chat apps or stock tickers.
Introducing Action Cable
Rails was somewhat late to the WebSockets game, until Action Cable was introduced with Rails 5 in 2015. Rails creator DHH called it “the highlight of Rails 5”, and after taking some time to experiment with it, I can see why. Action Cable simplifies the process of implementing WebSockets and lets you incorporate real-time features into your apps fairly easily. It may not be the most intuitive thing in the world, but once you get the hang of it, it’s not bad at all.
With that said, let’s dive right in!
Setting Up Your Rails API
First, navigate to the directory you want to create your app in and generate your rails app:
Rails new my-app-api --api -T -d postgresql
The --api
flag generates a more slimmed down version of the app, without many of the front-end-related files that would normally be generated (we don’t need these since we’ll be generating a separate React app for the front-end). The -T
flag skips the creation of Rails’ test framework, which is unnecessary for our purposes, while -d postgresql
configures the app to use PostgreSQL instead of the default SQLite (which I recommend because it’ll make things easier if you want to upload your app to Heroku down the line).
However, PostgreSQL does require a little a little more configuration than SQLite. If you haven’t used it before, you can check out how to do so here. If you have, then setup should be straightforward — just add your PostgreSQL username and password to both the development and test sections of your config/database.yml file and run rake db:setup
.
Since we’re generating our back-end as a standalone API, we’ll need to configure our CORS settings to allow requests from our front-end. Uncomment the line with gem 'rack-cors'
in the Gemfile and run bundle install
. Then go into config/initializers/cors.rb and edit it to look like this:
Setting origins to '*'
should be alright for development, but keep in mind that if you deploy to production you’ll want to change this value to match your front-end’s URI for security reasons.
Now we need to generate our models. For the purposes of this tutorial, I’m going to keep things super generic and make a simple chat app. We’ll have two models, Conversation and Message — conversations have many messages, and messages belong to conversations. In a real-world scenario, you’d of course want to also add authentication into the mix, with both conversations and messages being tied to specific users. But I’m going to skip that so as to keep things simple.
Let’s generate our models and migrate:
rails g model Conversation title
rails g model Message text conversation:references
rake db:migrate
Generating our Message model with the conversation:references
option configured our ‘app/models/message.rb’ file for us already. But we’ll need to edit the file for our conversation model:
So far so (probably) familiar. Now we’ll start incorporating Action Cable. Action Cable uses redis, so go ahead and uncomment the redis
gem in your Gemfile and run bundle install
. We’ll then add our routes:
What’s going on with line 6 here? Well remember that WebSockets are their own protocol, so we’re not going to be setting up any ‘GET’ or ‘POST’ routes for them. Instead, the /cable
endpoint is what our client will be using to instantiate the WebSockets connection with our server. Labeling this endpoint /cable
endpoint is simply convention.
Now we need to generate something new — channels. Channels can be thought of as a means to organize streams of data. When the client instantiates a connection to the server by sending an appropriate request to the /cable
endpoint, they’ll also specify which channel they wish to subscribe to. Likewise, when the server needs to send out new content, it’ll specify which channel to broadcast the new data to, which will send the data to all of that channel’s subscribers.
So let’s generate two new channels, one of which will be used for broadcasting newly created conversations:
rails g channel conversations
and the other which will be used for broadcasting any new messages that are added to a specific conversation:
rails g channel messages
If you take a look at the newly generated files in app/channels, you’ll see that both have subscribed
and unsubscribed
methods. We’re not going to do anything with unsubscribed
here — it’s for handling any cleanup when a connection is terminated. But we will need to edit the subscribed
methods as follows:
Notice the subtle difference between the two: the conversations channel is using stream_from
while the messages channel is using stream_for
. Really these are two different ways of doing the same thing, except stream_from
expects to receive a string as an argument, while stream_for
expects an object from the model. If you’d like to learn more about the differences between the two, you can read about it here.
We’re going to get to generating our controllers soon, but before we do I want to incorporate one more thing — ActiveModel Serializer. I’m trying as much as possible to eschew anything that isn’t strictly related to Action Cable, and Serializer isn’t. However, it is exceedingly helpful (it specifies which data from your models gets shared when you render/broadcast a particular object), and it’s not difficult to learn or incorporate, so I’m making an exception for it.
Copy and paste the following into your Gemfile, as per the ActiveModel Serializer documentation:
gem 'active_model_serializers', '~> 0.10.0'
And run bundle install
. Then generate your serializers:
rails g serializer conversation
rails g serializer message
This creates two new files within the app/serializers directory. We can edit these to specify which fields from our models we want our API to share:
All set up! Now whenever the server sends data about a conversation to the client, it’ll only include the id
and title
fields for that conversation as well as an array of any associated messages. Likewise, those messages will only include each message’s id
, conversation_id
, text
, and created_at
fields. These minimal changes are suitable for our current needs, but Serializer is even more useful in other circumstances, such as when you want to share a user’s details, but not necessarily their password.
We can now finally generate our controllers:
rails g controller conversations
rails g controller messages
The ConversationsController
’s index
route is going to be used for the front-end’s initial fetch request to receive the current existing conversations and their messages. The create
routes for both controllers will be used for saving received data and broadcasting that data to the appropriate channels.
Two interesting things to note here. One is that that ugly mess of code that gets assigned to the serialized_data
variables is necessary for using Serializer with WebSockets. Normally, we wouldn’t have to do this — we could simply write render json: conversations
as we did in the ConversationsController
’s index
method. However, since our create
methods are broadcasting the data to our channels rather than rendering the JSON directly, we need to instantiate new Serializer instances manually. It’s ugly, I know… but trust me, it’s worth it.
The second thing to notice is that we’re using two different methods to broadcast. The first is the ActionCable.server.broadcast
method used in the ConversationsController
, which accepts as arguments a string for which channel to broadcast to as well as the data to be broadcast (notice that this string corresponds to the string we provided in our ConversationsChannel
’s subscribed
method).
The second way of broadcasting data is the MessagesChannel.broadcast_to
method, which instead of a string takes an object from our models as it’s first argument. This object corresponds to the one we specified in the MessagesChannel
. In general, I find stream_for
and broadcast_to
to be useful for transmitting data along non-universal channels, such as for members of a particular conversation or specific users.
I also want to mention that we don’t necessarily need to put the logic for broadcasting within our controllers. We could’ve done this within the channels themselves, adding other methods on top of subscribed
and unsubscribed
, as this tutorial does here. Or we could’ve delegated this functionality to Active Jobs, telling our application to automatically broadcast whenever a message or conversation is created, as demonstrated here. But I’m including the logic within the controllers here because controllers are familiar, and I personally find it easier to learn new things within the context of the familiar.
With that, our back-end should be all ready to go. On to the front-end!
Setting Up React to Work With Action Cable
Let’s create our React app. Back in the directory above your API, run:
create-react-app my-app
We’re going to be using a module called react-actioncable-provider
to incorporate the Action Cable functionality into our front-end. I want to give a major shoutout to its creator, Li Jie — this package really simplifies and streamlines the whole process.
Run the following in your console:
npm install -S react-actioncable-provider
Before we get started creating our components, lets lay down some core variables we’re going to be using repeatedly. Within src, create a folder called ‘constants’ and within that, create a file called ‘index.js’. Fill it out like so:
We’re defining these here to avoid repeating ourselves and so that later, if we deploy to production and the URLs need to be changed, we only have to change them in one place. Notice that we have a separate URL for WebSockets, since they are their own protocol and do not use HTTP.
I want to approach this from the top down, so let’s start with our root ‘index.js’ file, located directly within our ‘src’ folder. We’re going to import the <ActionCableProvider>
component from the react-actioncable-provider
package, and use it to wrap our <App>
component like so:
We provide the <ActionCableProvider>
component with a URL which corresponds to the /cable
endpoint we set up in our Rails routes. We’ll then edit our <App>
component:
I’ve removed everything that was inside of <App>
and replaced it within a soon-to-be-created component called <ConversationsList>
, which will effectively act as the top-level-component in charge of managing our application’s state.
Now it’s finally time to create the ‘components’ folder within ‘src’. We’re going to have five components. First, our top-level component, ConversationsList
:
This component fetches the data for any pre-existing conversations from our API when it mounts, and also has methods for adding new data to its state when the API broadcasts a newly created message or conversation. Within the render
method, we’re using a component called <ActionCable>
which was also imported from the react-actioncable-provider
package. It takes two props, channel
(Which specifies the channel to connect to, as well as any optional params as we’ll see in a bit) and onReceived
, which handles what to do with any broadcasted data.
Our <ConversationsList>
renders a list of <Cable>
components. Create ‘Cable.js’ within your ‘components’ folder and edit it as follows:
This component also renders <ActionCable>
components, but this time gives them an extra parameter within the channel prop that corresponds to a given conversation’s id. If you look back at the MessagesChannel
we defined in our back-end, you’ll see that we use this parameter to select a conversation from the model and instantiate a connection for that conversation.
I’m also using the <Fragment>
component here. Fragments were introduced in React 16.2, so they’re very new. Since our <ActionCable>
components don’t actually render anything themselves, it seems wasteful to wrap them in an empty <div>
— thus the use of <Fragment>
. But if you’re on an older version of React, you should be able to replace <Fragment>
with <div>
and the functionality will be identical.
These components contain everything we’ll need connect our app to Action Cable. The rest of the components are merely filling out what is necessary to make our app functional. As such I won’t go too into detail on the rest, since there’s nothing here that shouldn’t already be familiar from React. But you can still copy them:
And that’s it! Now if you create a new conversation/message, you should see it show up immediately. And it’ll persist, so if you refresh the page it’ll still be there.
Conclusion
This blog post is getting pretty long, so I’m going to end it here — with what we’ve covered, hopefully you’ve learned enough to be able to start incorporating Action Cable into your own React apps.
There are two other things I had wanted to cover. First, in order to really see the benefits of WebSockets, you’ll need to deploy to Heroku or some other platform where multiple people can use your app simultaneously. If that’s something you’re interested in, you can follow the steps here. The only additional steps you’ll need to take are changing the API_ROOT
constants we defined on the front-end in the ‘index.js’ file of the ‘constants’ folder, as well as potentially adjusting your CORS allowed origins settings on the back-end.
Secondly, I would highly recommend dissecting the functionality of the react-actioncable-provider
module we used. It’s short, only 142 lines of code, and it should help clarify exactly what these components are doing and how you could potentially recreate their functionality in a non-React environment.
Please feel free to reach out if you have any questions or suggestions. That’s all, folks!