Subscribing, Sending and Receiving ActionCable messages with vanilla JavaScript

ActionCable is an awesome feature of Rails that allows for sending real-time messages. If your frontend is part of your Rails app, much of the plumbing is handled for you. However, sometimes you need to use ActionCable, outside of Rails. In this blog post I’ll show you how to subscribe to a channel, print out messages and publish messages, all with vanilla JavaScript. Although I’m using JS, the principles apply to any language that supports websockets.

Establishing a connection and subscribing to a channel

1
2
3
4
5
6
7
8
socket = new WebSocket('ws://localhost:3000/cable');
socket.onopen = function(event) {
    const subscribe_msg = {
        command: 'subscribe',
        identifier: JSON.stringify({channel: 'GeneralChatChannel'}),
    };
    socket.send(JSON.stringify(subscribe_msg));
};

First, you need to establish a websocket connection. JavaScript has a build in API for that. The WebSocket constructor takes a websocket url. You can’t use http, it must be a ws or wss protocol url. In this example I’m using localhost but you can replace it with any websockets url.

Once you’ve used the constructor to return a WebSocket object you have access to onopen, which allows you to run a callback function once the websocket connection has been established. Inside that function you can write all the logic for subscribing to channels and receiving or publishing messages. In this case, I’m going to start off by subscribing to GeneralChatChannel. To do that, I’ll call the send method on my WebSocket object. I’ll pass send a JSON object with two fields:

Logging out incoming messages

1
2
3
4
5
6
7
socket.onmessage = function(event) {
    const incoming_msg = JSON.parse(event.data);

    if (incoming_msg.type === "ping") { return; } // Ignores webhook pings.

    console.log("FROM RAILS: ", incoming_msg);
}

Once subscribed to a channel, you can use another WebSocket method to log out incoming messages. onmessage allows you to define a callback function, which gets executed whenever a message is received (on the channel you’ve subscribed to). The callback function receives an event that has a data property containing the message data. You can simply parse the JSON and log it out.

One thing to note here is the middle line of the function body. onmessage will receive regular pings, the purpose of which is to keep the websocket connection alive. These ping messages don’t contain message data, they’re low level events that can be ignored.

Sending a message

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (msg.type === "confirm_subscription") {
    const msg = {
    command: 'message',
    identifier: JSON.stringify({channel: 'GeneralChatChannel'}),
    data: JSON.stringify(
      {
        user_id: 1,
        message: 'Hello world!'
      }
    )
  }

  socket.send(JSON.stringify(msg2));
}

You can also use the WebSocket object to send messages. Sending a message is simple, just craft some JSON and use the send function to publish it to an ActionCable channel. However, before sending a message you need to make sure the channel subscription has been fully established, otherwise, messages may get dropped. You can do that by waiting for an incoming message of type confirm_subscription. Once that’s received, you can go ahead and start sending messages.

send expects a JSON object with three fields:

Putting it all together

Feel free to use the subscribe, logging or publishing snippets on their own. Alternatively, you can combine all three operations into a single function. I’ve found this very useful for testing ActionCable from outside Rails:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
(function() {
    socket = new WebSocket('ws://localhost:3000/cable');
    socket.onopen = function(event) {
        const subscribe_msg = {
            command: 'subscribe',
            identifier: JSON.stringify({channel: 'GeneralChatChannel'}),
        };
        socket.send(JSON.stringify(subscribe_msg));
    };

    socket.onmessage = function(event) {
        const incoming_msg = JSON.parse(event.data);

        if (incoming_msg.type === "ping") { return; } // Ignores pings.

        console.log("FROM RAILS: ", incoming_msg);

        if (msg.type === "confirm_subscription") {
          const msg = {
            command: 'message',
            identifier: JSON.stringify({channel: 'GeneralChatChannel'}),
            data: JSON.stringify(
              {
                user_id: 1,
                message: 'Hello world!'
              }
            )
          }

          socket.send(JSON.stringify(msg));
        }
    };
})()