using actioncable for websockets with a streaming api endpoint
Tags: rails, actioncable, react, websockets
Published: 2022-03-25 14:18:59

## What are websockets?

Firstly, we know that HTTP is a unidirectional communication protocol that uses a request and response cycle. Each time a new request or response is sent, a new connection must be established. We see this often in fetch calls to RESTful APIs.

![Diagram of HTTP protocol vs websocket protocol](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x2m7i4brmrt1achc3ozn.png)

Just like HTTP protocol uses **http://** and **https://**, websockets use **ws://** and **wss://**. This diagram shows the major difference between HTTP and websocket connections.

After the request is sent in a websocket connection, instead of the connection closing and a response being initiated, the client and server have a handshake that allows the connection to be upgraded to websocket. This communication link is bidirectional, or full-duplex, and remains open so that messages can freely be exchanged between server and client without interruption until the connection is closed by either side.

## What can we do with them?

Websockets are used in all sorts of real-time applications. I recently developed a full stack application that lets users create event boards to follow hashtags, and use those boards to stream live tweets from Twitter by connecting to Twitter's Streaming API endpoint. I wanted to share how I got started using Ruby on Rails ActionCable for websockets on the back end and @rails/actioncable npm package for the React front end.

Ruby on Rails comes with a built in package called ActionCable which provides seamless websocket integration on the back end. Once a channel has been established, we are able to easily call on ActionCable to broadcast anything we'd like! 

To create this project I signed up for [Twitter Developer Credentials](https://developer.twitter.com/en/docs/twitter-api/getting-started/getting-access-to-the-twitter-api) and stored my API Keys in an `config/application.yml` file which I hid with a `.gitignore` file. I had decided on using Twitter's Filtered Stream endpoint, so I went to the [Twitter-API-v2-sample-code](https://github.com/twitterdev/Twitter-API-v2-sample-code/blob/main/Filtered-Stream/filtered_stream.rb) repo to get started. The repo contains examples of all of the different streaming endpoints and sample code in multiple languages.

> _The filtered stream endpoint group enables developers to filter the real-time stream of public Tweets. This endpoint group’s functionality includes multiple endpoints that enable you to create and manage rules, and apply those rules to filter a stream of real-time Tweets that will return matching public Tweets. This endpoint group allows users to listen for specific topics and events in real-time, monitor the conversation around competitions, understand how trends develop in real-time, and much more._
[developer.twitter.com](https://developer.twitter.com/en/docs/twitter-api/tweets/filtered-stream/introduction)

The sample Ruby code for connecting to the streaming endpoint looks like this: 
```ruby
# Connects to the stream and returns data (Tweet payloads) in chunks
def stream_connect(params)
  @options = {
    timeout: 20,
    method: 'get',
    headers: {
      "User-Agent": "v2FilteredStreamRuby",
      "Authorization": "Bearer #{@bearer_token}"
    },
    params: params
  }

  @request = Typhoeus::Request.new(@stream_url, @options)
  @request.on_body do |chunk|
    puts chunk
  end
  @request.run
end
```

At this point I was able to connect to the stream in my Rails console by running `TwitterStream.stream_connect()` which was very exciting. Further configuration is required to make use of the raw data our connection is receiving.

First we need to create the `TweetChannel`, which can be done manually or by running `rails g channel tweet` to have Rails generate one for you. Then we need to add in a little more information to let the channel know where to stream from. I go over this more below in the section on the front end setup.

```ruby
class TweetChannel < ApplicationCable::Channel
  def subscribed
   stream_from "tweet_#{params[:rule]}"
  end
end
```

In my `EventsController`, I have an action that is triggered by a front end fetch request, that calls `TwitterStream.stream_connect(event)` and passes in an event object with some properties that I use to modify the way this application connects to the Twitter streaming endpoint. The starter docs provided by Twitter are a great place to start but of course every application will have unique needs, and there is so much more we can add in ourselves.

```ruby
 def self.stream_connect(event)
    puts "Connecting to Twitter Stream"
      params = {
          "expansions": "attachments.media_keys,author_id,entities.mentions.username,geo.place_id,in_reply_to_user_id,referenced_tweets.id,referenced_tweets.id.author_id",
          "tweet.fields": "author_id,created_at,entities,attachments,geo,id,in_reply_to_user_id,lang,public_metrics",
          "user.fields": "name,username,verified,profile_image_url",
          "media.fields": "preview_image_url,alt_text,url"
          
      }

      options = {
          timeout: event.timeout,
          method: 'get', 
          headers: {
              "User-Agent": "v2FilteredStreamRuby",
              "Authorization": "Bearer #{BEARER_TOKEN}"
          },
          params: params
      }

      request = Typhoeus::Request.new(STREAM_URL, options)
      request.on_body do |chunk|
        ActionCable.server.broadcast("tweet_#{event.rule_id}", { body: chunk});
      end
      request.run
  end
```

So we can see that above in the original sample code, Twitter had `puts chunk` which printed to the console any incoming data, which I replaced with `ActionCable.server.broadcast("tweet_#{event.rule_id}", { body: chunk});`, which is sending the params `event.rule_id` to the `TweetChannel` we created above, and _also_ sending the `chunk` of raw data out to make its way to our front end. That single line of code is the game changer here, allowing us to simply broadcast anything coming through the websocket from Twitter, directly to the TweetChannel, and then on to our React front end.

Lastly, let's make sure to put `mount ActionCable.server => '/cable'` into our `config/routes.rb` to make sure we have somewhere to send a request for a websocket connection.

The [Ruby on Rails docs](https://guides.rubyonrails.org/action_cable_overview.html) has a lot of great information on how to use ActionCable in different parts of your application. But lets move over to the front end to see how all of this fits together.

### Front end

The first step is to install the  `@rails/actioncable` npm package so that we can connect the front end with our rails back end. Run `npm install @rails/actioncable` and we are ready to go.

```javascript
import { createConsumer } from "@rails/actioncable";
import { useRef } from "react";

export default function Event() {
  const cable = useRef();

  useEffect(() => {
    if (!cable.current) {
      cable.current = createConsumer("ws://localhost:3000/cable");
    }

    // lets the back end know the channel to broadcast on
    const channelObj = {
      channel: "TweetChannel",
      rule: event.rule_id,
    };

  const handlers = {
      received(data) {
      // here you can add code to do something with
      // the data received from the websocket connection
         },
      connected() {
        console.log("connected");
         },
      disconnected() {
        console.log("disconnected");
        cable.current = null;
         },
      };

    const subscription = 
      cable.current.subscriptions.create(
      channelObj,
      handlers
    );

    return function cleanup() {
      subscription.unsubscribe();
      cable.current = null;
    };
}, []);
```

Above is some very basic boilerplate code for getting started. Consumers **require** an instance of the connection on the client side. Using `createConsumer()` will get a connection to `/cable` ready on the server by default. But it won't actually do anything until you have specified a subscription. With our handlers and our channel object we are getting our parameters ready to be passed in to `cable.current.subscription.create()`. At the top of the file we create a variable `cable` and assign it to `useRef()`. This, combined with the code checking for `!cable.current`, ensure that we do not create a new subscription every time the component is re-rendered. This is going to help avoid glitchy behavior, and especially if in use with a chat style app, this will help avoid duplicate or out of order messages. The `useEffect` will make sure that this subscription is created, or found, on component renders.

The `channelObject` has 2 key/value pairs `channel: "TweetChannel"` and `rule: event.rule_id`, which correspond to the `TweetChannel` that we created earlier. When the subscription is created on the front end, it sends the information to the `TweetChannel` which it uses to `stream_from` the correct websocket connection.

```ruby
class TweetChannel < ApplicationCable::Channel
  def subscribed
   stream_from "tweet_#{params[:rule]}"
  end
end
```

In the received method of the handlers variable, I take the incoming data, create a new Tweet object, where it is then added to a Tweets array, and animated onto the screen.

This project was the absolute most fun to create, and if you would like to see more details about it, check out this [demo video](https://www.youtube.com/watch?v=urTOZf8Z2A4).

**Github repositories:**
Front end: https://github.com/e-papanicolas/tweet-front-end
Back end: https://github.com/e-papanicolas/tweet-back-end