Action Cable

Ruby on Rails Intermediate/Advanced
Course 2 ยท Chapter 6 ยท Action Cable

๐Ÿ“ก Action Cable

Chapter 5's JSON API is still request/response: the client asks, the server answers, the connection closes. Some features genuinely need the server to speak first โ€” a new comment appearing on everyone else's screen without a refresh, a live notification badge. express2-6 covered Socket.IO for exactly this in Express; Action Cable is Rails' own built-in answer to the same problem.

๐Ÿ”— Connections and Channels

# 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

    private

    def find_verified_user
      if (user = User.find_by(id: cookies.encrypted[:user_id]))
        user
      else
        reject_unauthorized_connection
      end
    end
  end
end

A Connection is established once per browser tab, the moment the WebSocket handshake happens โ€” it's where authentication lives, reusing the same encrypted session cookie rails2-1 already set during a normal HTTP login. identified_by :current_user declares that every message flowing through this connection knows which user it belongs to, and reject_unauthorized_connection refuses the handshake entirely for anyone who fails the check โ€” the WebSocket equivalent of rails2-1's require_login before_action.

A single Connection can host many Channels โ€” one per feature that needs real-time updates:

# app/channels/comments_channel.rb
class CommentsChannel < ApplicationCable::Channel
  def subscribed
    post = Post.find(params[:post_id])
    stream_for post
  end
end

stream_for post subscribes this client to a stream scoped to one specific post โ€” Action Cable derives a unique stream identifier from the object automatically, so every browser tab currently viewing that post's comments joins the same stream, and tabs viewing a different post don't.

๐Ÿ“ข Broadcasting

# app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :post
  after_create_commit :broadcast_comment

  private

  def broadcast_comment
    CommentsChannel.broadcast_to(post, CommentSerializer.new(self).as_json)
  end
end

after_create_commit is a callback (from Course 1's validations/callbacks chapter, rails1-7) that fires only after the database transaction actually commits โ€” broadcasting before that point risks telling clients about a comment that a later rollback then undoes. broadcast_to(post, ...) pushes the payload to every client streaming that specific post, reusing the exact same CommentSerializer Chapter 5 wrote for the JSON API โ€” one source of truth for "what a comment looks like," whether it arrives via a REST request or a live broadcast.

๐Ÿ’ป The Client Side

// app/javascript/channels/comments_channel.js
import consumer from "./consumer"

consumer.subscriptions.create(
  { channel: "CommentsChannel", post_id: postId },
  {
    received(data) {
      appendCommentToPage(data)
    }
  }
)

The client subscribes with the same post_id param the server's subscribed method reads, and received fires every time a broadcast arrives on that stream โ€” no polling, no manually re-fetching the comments list.

๐Ÿ“œ Action Cable vs Socket.IO

Rails Action Cable vs Express + Socket.IO (express2-6)

ConcernExpress + Socket.IORails Action Cable
Connection authio.use() middlewareConnection#connect, identified_by
Grouping clientsRooms (socket.join(room))Streams (stream_for / stream_from)
Sending a messageio.to(room).emit(event, data)Channel.broadcast_to(object, data)
Feature organizationEvent names on one connectionSeparate channel class per feature
Triggering a broadcastCalled explicitly wherever neededOften a model callback (after_create_commit)

The concepts map closely โ€” a Socket.IO "room" and an Action Cable "stream" solve the same "only these clients get this message" problem. The organizational difference is Rails' usual one: a dedicated channel class per feature (CommentsChannel, NotificationsChannel) instead of a flat set of event name strings on one shared connection.

๐Ÿ’ป Coding Challenges

Challenge 1: A Notifications Channel

Write a NotificationsChannel where subscribed streams for the current connection's current_user (not a param โ€” the user identified on the connection itself).

Goal: Practice streaming scoped to the authenticated user rather than a URL parameter.

โ†’ Solution

Challenge 2: Broadcasting on Create

Add an after_create_commit callback to a Notification model that broadcasts the new notification (serialized) to the NotificationsChannel stream for its owning user.

Goal: Practice triggering a broadcast from a model callback, tied to the right stream.

โ†’ Solution

Challenge 3: Client Subscription

Write the JavaScript client code subscribing to NotificationsChannel and appending each received notification to a #notifications element on the page.

Goal: Practice wiring the client side of a channel subscription to a broadcast.

โ†’ Solution

๐Ÿ’Ž Not Every Feature Needs a Socket

Action Cable is the right tool when data genuinely needs to appear without the user doing anything โ€” a live chat, a notification badge. If a page only needs "show me the latest data when I next visit or refresh," a plain request (Chapter 5's JSON API, or a normal page load) is simpler, has one less moving part (no persistent connection to manage), and is usually the better default.

๐ŸŽฏ What's Next

Next chapter: Hotwire โ€” Turbo & Stimulus โ€” how modern Rails apps add interactivity to server-rendered HTML without reaching for a full frontend framework.