Screenshot from Sketchit App

Drawing together in real-time with Action Cable and React

Wrapping your head around how Action Cable, Rails, React and Redux all fit together can be daunting but I’ll you how to set up a simple collaborative canvas that updates in real-time across multiple users.

Jesse Gan
6 min readOct 21, 2020

--

What is Action Cable?

If you want to dive into the official docs, head over to the Rails Guide.

Action Cable allows you to add real-time features to your applications. For example, features such as chat boxes, and notifications, or in our case, a real-time sketchpad. In other words, any time you want actions from one user to be reflected on another user’s page without needing to refresh, you can use Action cable.

https://giphy.com/gifs/tipsyelves-rainbow-magic-3o84U6421OOWegpQhq

The magic behind this are WebSockets.

A Brief Introduction to WebSockets

I won’t go into too much detail because there are a ton of resources out there to learn more about the WebSocket protocol. Here’s one I liked that has more links at the bottom as well.

The WebSocket protocol allows for continuous connection between a user and a server. This means data can be broadcasted and received from that connection without needing to reload the page as we would have had to do with the HTTP protocol. WebSockets can handle many clients and connections and make real-time applications a possibility.

Diagram of the WebSocket Protocol

** Pay attention to some of the bolded language, they’ll pop up later on ;)

Time to Build Our Drawing App

Figuring out how all the pieces fit together was the hardest part for me of learning Action Cable. I’ll try to lay it all out before and then dive into each piece of code. If you ever feel lost, check back here.

Here is the structure of our code:

  1. Consumers — When a client connects to our WebSocket, they become a consumer. In our frontend, we will need to connect our user to our WebSocket server in order to turn them into a consumer.
  2. Channels — These are our individual streams of data between the consumer (user) and the server. We will use channels to handle the individual streams of data for each canvas our users have open.
  3. Subscriptions — These are our connection between a consumer and a channel. A user can subscribe to multiple channels allowing them to receive data anytime sometime is broadcasted to that channel. We will create these subscriptions and define what to do when we receive data from it.
  4. Controllers — These are the same controllers we use in Rails but we’ll use them here to handle broadcasting data to our channels.

Note: There are many different ways to structure how you receive, handle, and broadcast data through Action Cable so this is just one way among many others you can find online.

Turning clients into consumers

Just like creating routes for our typical Rails application, we need to create a route that we can use to connect our users to the WebSocket server.

routes.rb

Adding that line into our routes.rb file will create a URL that we can use to connect to our WebSocket. In our case, it sets our WebSocket route to the following: ws://localhost:8000/cable but it would be set to wherever you host your application.

Moving in our react frontend, we add the following to index.js.

index.js

We’re doing a few things here:

  1. Creating and exporting a CableApp object that will store our consumer to be used in our components
  2. Using the actioncable package to create a consumer using the route we created above
  3. Storing that consumer into our CableApp object

Setting up our channels

Next, we setup channels for our consumer to subscribe to. Channels can be set up using a Rails model or using a string. In this case, we distinguish each channel with the lobby code of the canvas we want to subscribe to.

canvas_channel.rb

In another use case, we could’ve use a model to distinguish each channel. If you want to do that, you can use the following

def subscribed
canvas = Canvas.find(params[:id])
stream_for canvas
end

Notice the use of stream_for instead of stream_form!

Subscribing to our channels

Back in our react frontend, we want our consumer to subscribe to the canvas of the lobby they are in so that they can receive updates to the canvas.

CanvasContainer.js

So what’s happening here?

CableApp.canvas = CableApp.cable.subscriptions.create({
channel: “CanvasChannel”,
lobby_code: this.props.lobbyCode
}

Line 42–45: We are create a subscription to the CanvasChannel while passing in the lobby_code parameter that we use in our subscribed method above. We do this by importing CableApp (remember we exported it in our index.js file), getting the consumer we stored in CableApp.cable, and finally creating a new subscription using subscriptions.create().

The first parameter of create() requires a key named channel with the name of the Channel the consumer is connecting to. Other keys within that object will be passed in as parameters to our Channel.

{
received: ({ type, data }) => {
switch(type){
case("draw"):
// draw()
break
case("clear"):
// clear()
break
default:
console.log("No action")
}}
}

Line 46–58: As a second parameter in create(), we can define how to handle data that is broadcasted to the channel. The received key takes a callback that gets the received data passed in.

In our case, every time we receive data with a type equal to "draw", we will call the draw method on our CanvasContainer component. And because WebSockets are continuous connections, no refresh is required when we receive data so these updates to the canvas happen seamlessly… like magic!

Finally we broadcast data through our controllers

The last bit is where we broadcast data to the appropriate canvas channel.

In our CanvasController, we set up an action called draw that will broadcast data to the right CanvasChannel.

canvas_controller.rb

The first parameter of the broadcast() method is the name of the channel and the second parameter is the data to be passed to that channel. See how the object keys ({type, data}) match the keys we have in our received data callback in the last section.

So how do we make sure we’re broadcasting every time a user is drawing?

In CanvasContainer.js, we have the following code in our handleMouseMove() method:

CanvasContainer.js

This sets up the data we need to broadcast to our channel and then called our updateCanvas() which you can see here:

canvasActions.js

In this action, we perform a POST request that hits our draw action we defined above which in turn, broadcasts the drawing onto the screen of every consumer that is connect to that CanvasChannel.

Wrapping it all up

So to recap the flow data:

  1. User connects to app and we create an Action Cable consumer
  2. User opens up a lobby and loads the canvas. We use the consumer to subscribe to the canvas channel of the lobby they are in.
  3. A user across the world joins the same lobby and starts drawing.
  4. Every time they draw (moves their mouse), we fire the draw action in our canvas controller which broadcasts that drawing data to the canvas channel.
  5. User receives that data because they have subscribed to the channel and called the received callback that we defined when subscribing to the channel.
  6. Things are magically drawn on the user’s canvas.

Resources

If you want to see the whole project that I pull this code from, check out the GitHub to my project called Sketchit.

If you want to see the drawing app working, check out the video demo.

Thanks for reading! Follow for more coding and tech articles :)

--

--

Jesse Gan

Software Engineer helping companies bring new products to life. Follow along with my coding journey here!