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:
With ActionCable, you create an open connection between your client and your server, allowing a communication flow:
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:
But with the open connection from ActionCable, they will get instant updates for that post 🔥
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
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
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
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”)
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
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();
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>
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:
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)
}
});
(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:
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
Top comments (2)
Great write-up! You might find CableReady to be an excellent next step in your exploration. I firmly believe that ActionCable is consistently undervalued as a framework in Rails.
Thanks Lukas! I like your way of explaining things. Please keep it up!