Rails 6 includes a lot of functionality out of the box to easily create publish/subscribe (pub/sub for short) features for your web app through the ActionCable library. Such a requirement is common these days for modern web apps. For example, let's say that you want to implement a wall feed similar to Facebook wherein as new posts are created in the system, these posts will appear in the feed without having to refresh the page. In this guide, you will go through the process of creating a pub/sub mechanism for rendering new posts in a wall feed using ActionCable.
Let's first establish the basic concepts of a publish/subscribe system in order to understand the different components of ActionCable.
A channel is any stream or location where a client program can connect to start receiving messages. For this guide, you will be creating a wall_channel
. A message broadcasted to this channel will be a new post that was saved in the system.
A broadcaster is any component that can send a message to a channel. A message in Rails is programatically represented as a hash object.
A client connects to a channel as a subscriber. Once connected, if the broadcaster sends a message through the channel, the subscriber will be able to see the message. In terms of implementation, javascript
code loaded in a page is used to subscribe to a channel and provide a callback function that processes the received message.
Start off by creating a new Rails 6 project. You can call it anything you want, but for purposes of this guide, the project will be named acdemo
:
1$ rails new acdemo
You should find a configuration file for ActionCable
over at config/cable.yml
. If you're working on the development environment, the default adapter
used is called async
. The adapter
is the queueing mechanism used by ActionCable to store messages to be broadcasted to the different channels. async
tells the framework to use in-memory data storage, which might not immediately work for some environments. It is recommended to use a locally running Redis server to handle the queueing.
This is the default configuration in cable.yml
for development
:
1development:
2 adapter: async
Change it to the following to tell ActionCable to use Redis:
1development:
2 adapter: redis
3 url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
If you don't have a Redis server running yet, install it for your platform of choice:
Make sure you have brew
installed.
1$ brew update
2$ brew install redis
3$ brew services start redis
1$ sudo apt install redis-server
2$ sudo service redis-server restart
Redis unfortunately is not available for Windows. However, you can install Ubuntu within Windows via WSL2. Installation instructions can be found here.
To create a channel called wall_channel
, issue the following command:
1$ rails g channel wall
The command will generate the following important files:
app/channels/wall_channel.rb
: The backend component representing a channel. For now, make sure that the stream_from "wall_channel"
method is called within the subscribed
method.app/javascript/channels/wall_channel.js
: Client code that contains different event handler methods for when you subscribe to a channel.The convention is that the name of the channel in the command issued earlier is wall
, which translates to wall_channel
on the Rails backend but is called WallChannel
on the client code.
Your app/channels/wall_channel.rb
code should look like the following:
1class WallChannel < ApplicationCable::Channel
2 def subscribed
3 stream_from "wall_channel"
4 end
5
6 def unsubscribed
7 # Any cleanup needed when channel is unsubscribed
8 end
9end
In the file app/javascript/channels/wall_channel.js
, you will find three functions. The connect()
function will be triggered as your client code subscribes to the WallChannel
. For now, you can validate that it is indeed subscribing to the channel by logging to the console.
1connected() {
2 console.log("Connected to WallChannel");
3}
You can leave the second function, disconnected()
, alone for now, but basically that's where you put additional client code after a subscription to a channel has been terminated.
The third function, received(data)
, is where the client code processes incoming data broadcasted to the channel, which is represented by the data
parameter. In this case, data
represents a newly created post that you can append to the page's interface. Put in the following code, which will create HTML elements representing a post and append them to a <div>
with id wall
.
1received(data) {
2 wall = document.getElementById('wall');
3
4 wall.innerHTML +=
5 "<h2>" + data.title "</h2>";
6
7 wall.innerHTML +=
8 "<p>" + data.content + "</p>";
9}
Your app/javascript/channels/wall_channel.js
should look like the following:
1import consumer from "./consumer"
2
3consumer.subscriptions.create("WallChannel", {
4 connected() {
5 // Called when the subscription is ready for use on the server
6 console.log("Connected to WallChannel");
7 },
8
9 disconnected() {
10 // Called when the subscription has been terminated by the server
11 },
12
13 received(data) {
14 // Called when there's incoming data on the websocket for this channel
15 wall = document.getElementById('wall');
16
17 wall.innerHTML +=
18 "<h2>" + data.title "</h2>";
19
20 wall.innerHTML +=
21 "<p>" + data.content + "</p>";
22 }
23});
Create a default root page that will "stream" created posts as they are received from the WallChannel
.
In the file config/routes.rb
, put in the following line:
1root to: "pages#index"
Create the file app/views/pages/index.html.erb
with the following code:
1<h1>Wall Posts</h1>
2<div id="wall">
3</div>
By default, Rails 6 will already load all JavaScript channel code as called by application.js
in the line require('channels')
. As a result, your app/javascript/channels/wall_channel.js
will be loaded in all pages of the application. You can test this out by starting the Rails server and refreshing the root page. You should notice in the console that printout of Connected to WallChannel
as programmed in the previous step. This indicates that the client code (subscriber) is now connected to WallChannel
.
Create a model called Post
with attributes title
and content
, both of type string
.
1$ rails g model Post title:string content:string
2$ rails db:migrate
Service objects are used as modular components that perform a specific task. For this app, a service object will be used to perform the creation of the post itself, persist it in the database, and broadcast the created post to the wall_channel
.
First, create a directory called app/services
where your service objects will reside. Add the following in config/application.rb
(within the class Application
):
1config.autoload_paths += %W(
2 #{config.root}/app/services
3)
This will tell Rails to load all classes within app/services
as part of its context and can be callable from anywhere in the system.
Create a service object called PostsCreator
in the file app/services/posts_creator.rb
:
1class PostsCreator
2 def initialize(title:, content:)
3 @title = title
4 @content = content
5 end
6
7 def create
8 post = Post.create!(title: @title, content: @content)
9
10 ActiveCable.server.broadcast(
11 "wall_channel",
12 {
13 id: post.id,
14 title: post.title,
15 content: post.content
16 }
17 )
18 end
19end
The constructor accepts title
and content
parameters to be used in the creation of a post. The create
method contains the primary logic of first creating a post and broadcasting it. Broadcasting data to a channel involves an invocation to ActiveCable.server.broadcast()
. The first argument is the channel name—in this case, wall_channel
. The second argument is the payload—in this case, a hash containing the id
of the created post, its title
, and its content
.
Start your Rails server and, from your browser, visit http://localhost:3000
. With your service object, you can now test the system by going into your Rails console and typing the following:
1> posts_creator = PostsCreator.new(title: "Sample Title", content: "Sample content")
2> posts_creator.create
Notice that received(data)
will be triggered and, as a result, append the received data
to the page, creating a streaming effect of wall posts on every create. You can further test this by opening multiple windows for the same page. Create more posts from the console using the same code as earlier, and notice that all the windows simultaenously update the wall interface with the created posts.
You now know the basics of creating publish/subscribe functionality for your Rails application. ActionCable makes it extremely convenient to set up all the needed components for both backend code as well as initial JavaScript client code. For more practice, try out the following:
wall_channel.js
code to a specific page and not globally (i.e. a /wall
route specifically to view all wall posts)Notification
that will serve as notification messages for your system. Your app should be able to alert subscribers as new notifications are created, regardless of where they are in the system.