Author avatar

Raphael Alampay

Updating a Rails App Wall Feed in Real Time with ActionCable

Raphael Alampay

  • Sep 24, 2020
  • 10 Min read
  • 69 Views
  • Sep 24, 2020
  • 10 Min read
  • 69 Views
Web Development
Ruby on Rails
Back End Web Development
Server-side Frameworks

Introduction

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.

Basic Concepts

Let's first establish the basic concepts of a publish/subscribe system in order to understand the different components of ActionCable.

Channel

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.

Broadcaster

A broadcaster is any component that can send a message to a channel. A message in Rails is programatically represented as a hash object.

Subscriber

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.

Setup

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
bash

ActionCable Configuration File

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:

1
2
development:
  adapter: async
yaml

Change it to the following to tell ActionCable to use Redis:

1
2
3
development:
  adapter: redis
  url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
yaml

Redis Server

If you don't have a Redis server running yet, install it for your platform of choice:

Mac OS X

Make sure you have brew installed.

1
2
3
$ brew update
$ brew install redis
$ brew services start redis
bash

Ubuntu

1
2
$ sudo apt install redis-server
$ sudo service redis-server restart
bash

Windows

Redis unfortunately is not available for Windows. However, you can install Ubuntu within Windows via WSL2. Installation instructions can be found here.

Creating a Channel

To create a channel called wall_channel, issue the following command:

1
$ rails g channel wall
bash

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:

1
2
3
4
5
6
7
8
9
class WallChannel < ApplicationCable::Channel
  def subscribed
    stream_from "wall_channel"
  end 

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end 
end
ruby

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.

1
2
3
connected() {
  console.log("Connected to WallChannel");
}
javascript

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.

1
2
3
4
5
6
7
8
9
received(data) {
  wall = document.getElementById('wall');

  wall.innerHTML +=
    "<h2>" + data.title "</h2>";

  wall.innerHTML +=
    "<p>" + data.content + "</p>";
}
javascript

Your app/javascript/channels/wall_channel.js should look like the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import consumer from "./consumer"

consumer.subscriptions.create("WallChannel", {
  connected() {
    // Called when the subscription is ready for use on the server
    console.log("Connected to WallChannel");
  },  

  disconnected() {
    // Called when the subscription has been terminated by the server
  },  

  received(data) {
    // Called when there's incoming data on the websocket for this channel
    wall = document.getElementById('wall');

    wall.innerHTML +=
      "<h2>" + data.title "</h2>";

    wall.innerHTML +=
      "<p>" + data.content + "</p>";
  }
});
javascript

Creating a Root Page

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:

1
root to: "pages#index"

Create the file app/views/pages/index.html.erb with the following code:

1
2
3
<h1>Wall Posts</h1>
<div id="wall">
</div>
html

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.

Creating a Model

Create a model called Post with attributes title and content, both of type string.

1
2
$ rails g model Post title:string content:string
$ rails db:migrate
bash

Creating a Service Object

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):

1
2
3
config.autoload_paths += %W(
  #{config.root}/app/services
)
ruby

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class PostsCreator
  def initialize(title:, content:)
    @title    = title
    @content  = content
  end

  def create
    post = Post.create!(title: @title, content: @content)

    ActiveCable.server.broadcast(
      "wall_channel",
      {
        id: post.id,
        title: post.title,
        content: post.content
      }
    )
  end
end
ruby

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.

Testing the Broadcast

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
2
> posts_creator = PostsCreator.new(title: "Sample Title", content: "Sample content")
> 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.

Conclusion

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:

  1. Isolate the wall_channel.js code to a specific page and not globally (i.e. a /wall route specifically to view all wall posts)
  2. Create another model called 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.

2