ActionCable: The Missing Guide

Development

Reading Time: 7 minutes

ActionCable was introduced to Rails as of version 5. It allows you to create pub/sub WebSocket connections in your Rails application, which brings live updates to your user experience. ActionCable upgrades an HTTP connection between the server and client to a WebSocket.

Some of the benefits of a WebSocket is that the amount of data transferred per transmission is considerably less as it removes unnecessary header/packet data, which is great for scalability. Another benefit is the server can push updates without the client requesting it. This can make for a much more engaging user experience.

If you need server-side triggered updates, multi-user interaction, or simply more dynamic content, ActionCable might be a good fit for you. Let’s unpack some of the basics surrounding this solution for integrating WebSockets with your Rails application.

The Terminology of ActionCable

Keep in mind that these definitions of commonly used words associated with ActionCable are as I understand them to be correct.

  • Cable – A cable is the one, as in only, WebSocket connection used to communicate all data for all channels.
  • Channel – Channel is a named, organized way to define behavior with both server and client methods, by which a client browser can subscribe and then communicate both ways via custom data handling code. Server-side implementation logic is kept here for subscriptions.
  • Connection – Server-side logic for subscription actions taken from clients browser.
  • Subscription – When a client initiates a “create subscription” event, the server keeps them grouped in a list of channel’s subscribers to publish to when data is sent via the given channel. Client-side implementation logic is kept here per named channel subscription.

The Layout

The server must have a Cable endpoint in the router by which all data can be communicated.

# config/routes.rb
mount ActionCable.server => '/cable'

The browser must have a connection request performed when loaded.

// app/assets/javascripts/cable.js
//
//= require action_cable
//= require_self
//= require_tree ./channels

(function() {
  this.App || (this.App = {});

  App.cable = ActionCable.createConsumer("/cable");

}).call(this);

Next we have the server-side connection logic for when a user’s browser subscribes to a channel.

# app/channels/application_cable/connection.rb
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
    end 

    protected
    def find_verified_user
      if current_user = User.find_by(id: cookies.signed[:user_id])
        current_user
      else
        reject_unauthorized_connection
      end 
    end 
  end 
end

And for channels, there are three locations of interest. First is our base class for all channels. This will be inherited/subclassed by all channels.

# app/channels/application_cable/channel.rb
module ApplicationCable
  class Channel < ActionCable::Channel::Base
  end
end

This is where you can define common methods that will be helpful across any or all of your channels.

Next are the channels themselves. Based roughly on the official Rails ActionCable READMEChatChannel example, we have the server-side code.

# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
  def subscribed
    stream_from specific_channel
  end 

  def receive(data)
    ActionCable.server.broadcast \
      specific_channel, format_response(data)
  end 

  private
  def specific_channel
    "chat_#{params[:room]}"
  end

  # Limit text to 140 characters
  def filter msg
    msg.to_s[0...140]
  end

  def format_response data
    {
      message: filter( data["message"] ),
      username: current_user.username
    }
  end 
end

`

This ChatChannel allows for multiple rooms (channels) to be subscribed to. You also get to reuse the code for each. This is possible because of the changeable room provided via params in the specific_channel method.

The receive method above takes the incoming user input and broadcasts it to all subscribers on the current channel. It’s advisable to process and filter data going through a chat app, so I’ve provided a brief snippet above on how you may do that.

And the client side code.

# app/assets/javascripts/cable/subscriptions/chat_lobby.coffee
App.chatChannel = App.cable.subscriptions.create { channel: "ChatChannel", room: "Lobby"},
  received: (data) ->
    @appendLine(data)
    $('#chat-feed').stop().animate{ scrollTop: $('#chat-feed')[0].scrollHeight }, 800

  appendLine: (data) ->
    html = @createLine(data)
    $("[data-chatroom='Lobby']").append(html)

  createLine: (data) ->
    """
    <article class="chat-line">
      <span class="speaker">#{data["username"]} :</span>
      <span class="body">#{data["message"]}</span>
    </article>
    """

$(document).on 'keypress', 'input.chat-input', (event) ->
  if event.keyCode is 13
    App.chatChannel.send
      message: event.target.value
      room: 'Lobby'
    event.target.value = ''

Then there is the view that has the input and contains the content for the chat.

html-actioncable

With this, you should have a fully working chatroom to test out (assuming you’ve already implemented the controllers, views, and user model with devise).

This implementation is stateless as is. Any time the user refreshes or changes pages, all previous chat information is lost. To persist state, you’ll need to integrate something like Redis. Of course, having the chatroom be stateless could be considered a “feature,” since you may only need to see conversations while you’re there.

Shared Channel Helpers

Now, in the code block above for app/channels/application_cable/channel.rb, I didn’t have any code written in there; we’ve only implemented one channel. But this file is the perfect place for common code — much like with helpers for views.

Here’s a method I recommend adding in instead of the specific_channel method I implemented earlier.

def stream_channel
  "#{channel_name}_#{params['room'] || 'main'}"
end

The channel_name method is a name helper included in ActionCable that’s a rough version of the channel class name. So FooChats::BarAppearancesChannel.channel_name will evaluate to 'foo_chats:bar_appearances'. The params['room'] || 'main' part will use a room name if it’s available in the parameters; otherwise it will use ‘main’. This method won’t care what kind of channel implementation you’re using.

In any channel, broadcast with stream_channel and you won’t have to worry about params. Just be sure to use the same method for stream_from and broadcast.

Sign up for a free Codeship Account

Partial Content Reloading

You may render and redraw parts of a page from a server broadcast, but you should weigh the costs for doing this. If you’re doing a multiuser broadcast and sending rendered partial HTML to everyone, you are losing your scalability advantage. But if you’re doing individual user subscription partial view updates, then that’s probably a much more feasible option.

Nithin Bekal in his ActionCable post does a similar chat channel as I’ve shown above, but he instead calls ApplicationController.render from within his channel code to render an HTML partial before broadcasting it down to all subscribed users. At that point, the HTML code is appended within the chat window.

This is an elegant solution for a developer, as the code is well organized and simple. But this is where you need to weigh the costs and know what you’re willing to permit broadcast capacity-wise (the smaller the feature, the smaller the cost).

Another option is broadcasting some JavaScript directly with something like this in your client-side CoffeeScript code:

received: (data) ->
  eval(data['js']) if data['js']

This will execute any JavaScript the server broadcasts out if it exists under the ‘js’ key in the data received. I’m not sure about this being a good idea though; it kind of feels wrong to me.

For a more performant website, it’s best to preload templates you’re going to use on the client and then render the data with it as it comes. You can defer retrieving the templates until after the page has completely loaded so it won’t be at the expense of the user’s experience in loading time.

If all you need is to update content only for the individual user after an action is taken, then I recommend using UJS. But if content needs to be updated without action from the user viewing the site, then ActionCable is likely the right tool for the job.

Perform

ActionCable provides @perform, which is available in your CoffeeScript code to directly call Ruby methods from your Channel’s code.

So if you wrote a method in your channel’s Ruby code to write to your logger when log_info is called, then in CoffeeScript, you can simply call it with @perform 'log_info', message: "Example!". Your server will have the hash passed to log_info as its parameter and then execute your code!

Browser Tools

The Google Chrome browser has WebSocket inspection built in. To access it, follow these steps:

  1. Load the inspect feature with Ctrl+Shift+I.
  2. Click on Network.
  3. Click WS.
  4. Once this is open, reload the page with F5. You will see a WebSocket Cable connection on the left side.
  5. Click on that Cable connection and you will see four tabs: Headers, Frames, Cookies, and Timing.
  6. Click on Frames. You will now see all communications done over the WebSockets Cable.
  7. Go ahead and type in messages in the chat on the page and see the communications broadcast out and in.

For Firefox, you need to install an add-on WebSocket Monitor. This tool is absolutely beautiful! It has a conversation mode which resembles text messaging; it organizes who said what by placing conversation windows either on the left or the right. It’s definitely a must-have tool.

Summary

This is a shorter blog post than some may expect on this topic of ActionCable. But I feel the most helpful things to understand first are the terms you’ll use. Once you grasp those, everything else starts falling into place: where things belong, and their purpose. After all, ActionCable got a lot of attention during its design phase, so there are some outdated blog posts out there that contain some inaccurate information. It’s best to start from the source to avoid confusion.

Beyond the basics, I also wanted to share that you could use ActionCable for pretty much anything. I wanted to show you how could actually use it, without focusing too much on the setup. This blog post should have enough meat and substance to provide a clear picture of what ActionCable is and empower your mind to come up with many brilliant possibilities.

Subscribe via Email

Over 60,000 people from companies like Netflix, Apple, Spotify and O'Reilly are reading our articles.
Subscribe to receive a weekly newsletter with articles around Continuous Integration, Docker, and software development best practices.



We promise that we won't spam you. You can unsubscribe any time.

Join the Discussion

Leave us some comments on what you think about this topic or if you like to add something.

  • Joshua Plicque

    This example isn’t working.

    I think the file location for: app/assets/javascripts/cable/subscriptions/chat_lobby.coffee

    should be

    app/assets/javascripts/channels/chat_lobby.coffee