loading...

Connect an Autonomous JS App to ActionCable for Realtime Goodness

johnlukeg profile image John Luke Garofalo ・5 min read

A few months ago I started learning Rails Action Cable for Fixt so that I could implement desktop notifications for our customer service reps. For those of you who don't know about Action Cable, it's an out-of-the-box websockets framework built for (and now into) rails. This was pretty straight forward as there are plenty examples in Ruby on Rails' preferred coffeescript, connecting via the asset pipeline. But as someone with somewhat of a shallow knowledge of websocket trends, I began to ask myself what if you want to connect from another standalone web app? Finally I had to face this question as I was tasked with connecting our Repair Tracking Component to our Ruby on Rails backend for real time tracking. This is where I started to explore the best way to utilize Action Cable from any Javascript app.


Submit a phone or tablet repair with Fixt to see the full feature for yourself ;)


Action Cable Setup

This isn't going to be a full tutorial, meaning I won't include all of the setup of action cables because this has been done well by the Edge Guides already. Instead I will focus on the changes you need to make to your configuration if you already have the standard action cable setup.

You'll need to do two things to make sure that you can connect to your websocket from an outside source. First you'll need to add your client's address to the list of allowed origins.

# config/environments/development.rb
config.action_cable.url = 'http://localhost:3000/cable'
config.web_socket_server_url = 'ws://localhost:3000/cable'
config.action_cable.allowed_request_origins = [
   # Local address of our RoR server
  'http://localhost:3000',
   # Local address we use for our React standalone client
  'http://localhost:8000',
]
# config/environments/production.rb
config.websocket_server_url = 'wss://<YOUR_SERVER_SITE>/cable'
config.action_cable.allowed_request_origins = [
   # Address of our Ruby on Rails App
  'https://<YOUR_SERVER_SITE>',
   # Address of our JS App
  'https://<YOUR_CLIENT_SITE>',
]

Note: I'm assuming that you're using ssl certificates in production, but if you're not, then just change https to http and wss to ws

If you haven't already, you'll need to setup a channel to track whichever model that you want to receive updates about. There are two paradigms in Action Cable, you can either stream for an Active Record object or you can stream to a channel in general. The differences are better explained in the Edge Guides Section 5.1 Streams. For the sake of simplicity, I'll explain what we want to do. We essentially want to stream any updates, to an instance of a model, to any client that's interested in that model. In our case at Fixt, we wanted to be able to track updates to a specific Repair instance.

# app/channels/repair_tracking_channel.rb
class RepairTrackingChannel < ApplicationCable::Channel
  def subscribed
    stream_for repair
  end

  def repair
    Repair.find(params[:id])
  end
end

Now, any time that we want to update the client that's interested in the repair when something changes, all we have to do is call something like this:

RepairTrackingChannel.broadcast_to(@repair, repair: repair.as_json)

Note: You don't have to use as_json. We actually use jbuilder at Fixt but since this article isn't about serializing data, I didn't want to spend too much time on it.


Javascript Setup

Now that we have Action Cable configured to stream to our standalone client JS app, let's set the client up. Everything before now has been vanilla Action Cable so this next part is the fun part.

Action Cable is just a layer on top of web sockets so you really can connect to it just using good ole JS web sockets. For this article, I'm just going to use the actioncable npm package because it makes the boilerplate web socket code a little easier to understand. If you are an anti-dependency, hardcore, 100x brogrammer who thinks npm is for the weak, then you probably don't need this tutorial or anyone's help, because you are unequivocally intelligent and we will all clap as you exit this thread.



Not you Hermione, you're perfect. <3

Let's go ahead and install actioncable to our project.

$ npm i -S actioncable

Then, let's create a file called repair-tracking-subscription.js

$ touch repair-tracking-subscription.js

With this file, we want to encapsulate all of our Action Cable channel logic, similar to how you would if you were connecting to ActionCable via the asset pipeline.

import ActionCable from 'actioncable';

// 1. Configure your websocket address
const WEBSOCKET_HOST = process.env.NODE_ENV === 'production' 
                         ? 'wss://<YOUR_SERVER_SITE>/cable' 
                         : 'ws://localhost:3000/cable';

export default function RepairTrackingSubscription(
  repairId, 
  { onUpdate = () => {} } = {}
) {
  // 2. Define our constructor
  this.cable = ActionCable.createConsumer(WEBSOCKET_HOST);
  this.channel;
  this.repairId = repairId;
  this.onUpdate = onUpdate;

  // 3. Define the function we will call to subscribe to our channel
  this.subscribe = () => {
    this.channel = this.cable.subscriptions.create(
      { channel: 'RepairTrackingChannel', id: this.repairId },
      {
        connected: this.connected,
        disconnected: this.disconnected,
        received: this.received,
        rejected: this.rejected,
      }
    );
  };

  // 4. Define our default ActionCable callbacks.
  this.received = (data) => {
    console.log(`Received Data: ${data}`);

    this.onUpdate(data);
  };

  this.connected = () => {
    console.log(`Tracking Repair ${id}`);
  };

  this.disconnected = () => {
    console.warn(`Repair Tracking for ${id} was disconnected.`);
  };

  this.rejected = () => {
    console.warn('I was rejected! :(');
  };
}
  1. This will be the ws/wss address that you set up in the previous section. You don't have to hardcode it in here but I am not going to presume to know your environment.
  2. For those unfamiliar with javascript functions and object oriented programming, this is our constructor and anything starting with this is a member variable on our object.
  3. We use the subscribe function to essentially invoke our call to our ActionCable server. You could do this in the constructor and save a step but I figured it's worth separating for understanding. Also this way would allow you to pass around a subscription and subscribe at your will.
  4. These are your ActionCable callbacks that are invoked by default when certain actions occur from the channel. You can read more about default and custom callback functions on the Edge Guides 5.3 Subscriptions.

That's it! Now we can track a repair from anywhere. Just import this function and subscribe like so:

import React, { Component } from 'react';
import repairTrackingSubscription from './repair-tracking-subscription';

class Tracker extends Component {
  state = {
    repair: {},
  };

  componentWillMount() {
    const { repairId } = this.props;

    const repairChannel = new RepairTrackingSubscription({ 
      repairId, 
      onUpdate: this.onRepairUpdate, 
    });

    repairChannel.subscribe();
  }

  onRepairUpdate = (data) => {
    const { repair } = data;
    this.setState(() => ({ repair }));
  }

  render() {
    const { repair } = this.state;

    return (
      <div>
        { JSON.stringify(repair) }
      </div>
    );
  }
}

export default Tracker;

Note: This is framework agnostic, you can use this function in any javascript situation that you currently find yourself in. I just currently find myself in React and can't seem to get out. Send help

Conclusion

This may seem simple to many of you but I legitimately wasn't sure how to connect to Action Cable from an autonomous app last week. Most of the tutorials out there made the assumption that you'd be working from within the Ruby on Rails framework. I hope that this helps some of you make some cool ish.

Posted on by:

johnlukeg profile

John Luke Garofalo

@johnlukeg

Founder of Front Yard Fantasy. I'm a full-stack software engineer who specializes in building products at startups.

Discussion

markdown guide