Skip to Content
Course content

88: Action Cable for WebSockets

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Up until now, our Rails apps have followed the classic request-response cycle: the user asks for a page, the server sends it, and the connection closes. But what happens when you need the server to push data to the user without them asking? That's where WebSockets come in, and in the Rails world, we handle that with Action Cable.

To see this in action, let's build a "Live Order Tracker" for a pizza shop. Imagine a customer is staring at their screen, waiting for their pizza to move from "Prepping" to "Out for Delivery." We don't want them hitting refresh every ten seconds; we want the status to flip the moment the chef clicks a button in the back office.

Mapping out the Order Channel

First, we need a Channel. Think of a channel as a specific "frequency" that a user can tune into. We don't want every customer seeing every pizza's status, so we'll create a channel that can be scoped to a specific order.

# app/channels/order_channel.rb
class OrderChannel < ApplicationCable::Channel
  def subscribed
    # We'll use the order_id passed from the frontend to create a unique stream
    stream_from "order_#{params[:order_id]}"
  end

  def unsubscribed
    # Clean up if necessary
    stop_all_streams
  end
end

By using stream_from, we're telling Action Cable: "Whenever a message is sent to the string order_123, send it to this specific browser connection."

Wiring up the JavaScript listener

Now we need the browser to actually listen. Action Cable provides a JavaScript consumer that handles the WebSocket handshake for us. I usually put this in a dedicated JS file or a Stimulus controller, but for the sake of clarity, here is the core logic.

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

consumer.subscriptions.create({ channel: "OrderChannel", order_id: 42 }, {
  connected() {
    console.log("Connected to the pizza tracker!");
  },

  received(data) {
    // This is where the magic happens. 
    // 'data' is the JSON payload sent from the server.
    const statusElement = document.getElementById("order-status");
    if (statusElement) {
      statusElement.innerText = data.status;
    }
  }
});

Triggering the broadcast from the model

The server side is easy. Whenever an Order is updated, we just need to shout that information out to the channel. I like putting this in an after_commit hook in the model so that the update is actually saved to the database before we tell the user about it.

# app/models/order.rb
class Order < ApplicationRecord
  after_commit :broadcast_status_update, on: :update

  private

  def broadcast_status_update
    ActionCable.server.broadcast("order_#{self.id}", { status: self.status })
  end
end

Fixing the "Everyone sees every pizza" bug

Now, here is where I messed up the first time I built something like this. In my initial draft, I used a generic stream name like "order_updates" for everyone. It worked great in testing with one user, but as soon as I had two customers, Customer A saw Customer B's pizza status change. It was a privacy nightmare.

I realized I was treating the stream as a category rather than a unique resource. The fix was moving to the interpolated string "order_#{self.id}" in both the channel subscription and the broadcast. Always remember: if the data is user-specific, your stream name must be unique to that resource or user. Never broadcast sensitive data to a global stream.

With this setup, the flow is seamless: Chef updates the order in the admin panel → Rails saves the record → after_commit triggers the broadcast → Action Cable pushes the JSON to the specific WebSocket → JavaScript updates the DOM instantly.




📋 Practical Task

Build a Real-time Notification Toast System

Your goal is to implement a notification system where a user receives a "toast" alert in the top-right corner of their screen whenever a new Comment is created on a post they authored.

  • Create a NotificationChannel that streams from a unique string based on the current_user.id.
  • Add an after_create_commit hook to the Comment model that broadcasts a message (containing the comment text) to the author of the post.
  • Write the JavaScript received(data) function to create a new HTML div with the comment text and append it to a #notifications-container on the page.
  • Ensure that the notification is only sent to the author, not to every user currently online.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.