A simple guide to Action Cable

lucaskuhn

Lucas Kuhn

Posted on May 6, 2021

A simple guide to Action Cable

Action cable is the Rails way of implementing WebSockets - with some Rails magic.

Repository for this app here

Why use it

Usually your client connects with your server by making requests:
953F8214-7173-4755-B16E-AB6FC9983223

With ActionCable, you create an open connection between your client and your server, allowing a communication flow:
65223B82-BB95-4D36-8FC0-7EE6B1227ED4

Example:

You have a simple blog - posts and comments - and multiple users reading that post. If one user adds a comment, the others will never know:
Kapture 2021-05-05 at 22.53.50

But with the open connection from ActionCable, they will get instant updates for that post 🔥
Kapture 2021-05-06 at 09.57.29

How to do it

First of all, generate a channel for your Posts. This class will be able to broadcast updates to all clients listening:



rails generate channel posts


Enter fullscreen mode Exit fullscreen mode

Which will create some files for you:



      create    test/channels/posts_channel_test.rb
      create  app/channels/posts_channel.rb
   identical  app/javascript/channels/index.js
   identical  app/javascript/channels/consumer.js
      create  app/javascript/channels/posts_channel.js


Enter fullscreen mode Exit fullscreen mode

Sending messages

We will work with our newly generated posts_channel.rb

We want to specify from which channel to stream, so we can pass an id params and ask rails to make a stream for that post:



class PostsChannel < ApplicationCable::Channel
  def subscribed
    post = Post.find(params[:id])
    stream_for post
  end
end


Enter fullscreen mode Exit fullscreen mode

And now, from anywhere in our app, we can call PostsChannel and ask it to broadcast something to anyone listening to that post:



PostsChannel.broadcast_to(@post, “hello world”)


Enter fullscreen mode Exit fullscreen mode

We will add this to our create action, to broadcast the comment to the post channel every time a comment is created:



# app/controllers/comments_controller.rb

def create
  @comment = @post.comments.new(comment_params)
    if @comment.save
      PostsChannel.broadcast_to(@post, @comment.body)
      redirect_to @post, notice: "Comment was successfully created."
    else
      render :new
    end
end


Enter fullscreen mode Exit fullscreen mode

And that does nothing so far since no one is listening to this broadcast. Moving forward!

Receiving messages

Opinionated setup:

I do not like to create a separate file for every consumer, I prefer to do the connection in script tags in the view. It feels more like a separate front end, where only the view that needs a connection creates one.
To do so, add this snippet in app/javascript/channels/index.js:



// Expose action cable
import * as ActionCable from '@rails/actioncable'
window.App || (window.App = {});
window.App.cable = ActionCable.createConsumer();


Enter fullscreen mode Exit fullscreen mode

Note: Exposing the cable was the default according to official docs until Rails 6, where Webpacker was introduced

The rails generator we used before created a file in app/javascript/channels/posts_channel.js
Here’s why we won’t use it:

  • It is always required, so whatever we put in it will run on every page of our app
  • We don’t want everyone opening a connection to get updates, just the people on our post show page

So you can go ahead and delete the created posts_channel.js đź—‘

And add a code to listen to our broadcast on the post show page:



<!-- app/views/posts/show.html.erb -->

<script>
  App.cable.subscriptions.create({ channel: "PostsChannel", id: "<%= @post.id %>" }, {
    connected() {
      console.log("Connected to the channel:", this);
    },
    disconnected() {
      console.log("Disconnected");
    },
    received(data) {
      console.log("Received some data:", data);
    }
  });
</script>


Enter fullscreen mode Exit fullscreen mode

And now, upon opening our blog post page, we can see the connected message on our terminal, and some Rails magic that enabled this connection:
82E5203A-8D8C-494E-8810-0CEACBF4AEAB

The posts:Z2lkOi8vYWN0aW9uY2FibGUtYXBwL1Bvc3QvMg is the name of the channel created by rails when we told it to stream_for post in our posts_channel file.

And you’re done! 🎉
~Almost~

While the above script received data, it doesn’t show it on the page. We can update it to add the comment to our list upon receiving them:



App.cable.subscriptions.create({ channel: "PostsChannel", id: "<%= @post.id %>" }, {
  received(comment) {
    el = document.createElement('li');
    el.innerHTML = comment;
    document.querySelector('ul').appendChild(el)
  }
});


Enter fullscreen mode Exit fullscreen mode

(All functions are optional, I removed the disconnected and connected ones)

And there you go, your app now talks to any browser listening to it via Action Cable:
Kapture 2021-05-06 at 09.57.29

Full code repo: GitHub: actioncable-app

References

Rails Guide:
https://guides.rubyonrails.org/action_cable_overview.html
Heroku Guide:
https://blog.heroku.com/real_time_rails_implementing_websockets_in_rails_5_with_action_cable
Cable.yml Config:
https://github.com/rails/rails/issues/28118
Cable for specific pages:
https://stackoverflow.com/questions/39597665/rails-actioncable-for-specific-pages
https://stackoverflow.com/questions/36438323/page-specific-actioncable
Good JS subscription examples:
https://stackoverflow.com/questions/39597665/rails-actioncable-for-specific-pages
https://samuelmullen.com/articles/introduction-to-actioncable-in-rails-5/
https://stackoverflow.com/questions/36266746/actioncable-not-receiving-data
Usage with ActiveJob
https://www.pluralsight.com/guides/creating-a-chat-using-rails-action-cable
Cable on ReactNative
https://stackoverflow.com/questions/43510021/action-cable-not-receiving-response-on-client
AnyCable
Action Cable vs AnyCable: fight! | Nebulab

đź’– đź’Ş đź™… đźš©
lucaskuhn
Lucas Kuhn

Posted on May 6, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related