Skip to content →

Stimulus + ActionCable + ActiveJob: Loading Asynchronous API Data


Interactivity. Every web app needs it.

And Rails comes with a number of tools, which, together, generate that feeling of a fast, responsive app, with mostly server rendered HTML and a little Javascript sprinkled on top. ActiveJob fetches data from a remote JSON api in the background. ActionCable routes data from the background job out to the front end, and Stimulus.js puts the new data right into place.

Here is a tutorial that wraps these three pieces together. It will pull the recent Rails Repository tags from Github and display them. It will then let someone select the tag, and load the commits using a previous tutorial’s Stimulus controller. It even has a challenge assignment at the end for a little extra practice.

Background Service Jobs

ActiveJob provides a mechanism to take work out of the main request-response cycle between the browser and the application server. As a way to show how multiple different people could be served without receiving the wrong data, a unique UUID is generated in the ruby controller when the page is rendered and passed to the ActionCable channel and to the ActiveJob.

rails g job LoadGithubCommits

The job loads in the tags from Github’s API, parse them to JSON, then passes them to a partial for rendering. ActionCable broadcasts the request back to the client whose request_id matches the ActionCable channel waiting for the commits.

class LoadGithubCommitsJob < ApplicationJob
  queue_as :default

  def perform(request_id)
    tags = JSON.parse Http.get("https://api.github.com/repos/rails/rails/tags").to_s

    ActionCable.server.broadcast "CommitTagsChannel:#{request_id}", {
      tags: CommitsController.render( partial: 'commits', locals: {tags: tags}).squish
    }
  end
end

ActionCable Channel

The CommitTagsChannel kicks off the job when a client subscribes to the channel. It passes along the request_id to keep track of the clients request.

rails g channel CommitTagsChannel

The channel, instead of the ActionController, is responsible for kicking off the job because a race condition occurs if the job is started by ActionController. The job may be finished before the ActionCable channel is setup, and the data would never be loaded by the client. Starting the background job via ActionCable guarantees the client is listening before anything is started off. There is nothing worse than speaking when no one is listening, and just as bad is processing data that cannot be sent over a WebSocket connection.

class CommitTagsChannel < ApplicationCable::Channel
  def subscribed
    stream_from "CommitTagsChannel:#{params[:request_id]}"
    LoadGithubCommitsJob.perform_later params[:request_id]
  end

  def unsubscribed
    stop_all_streams
  end
end

Stimulus – Commits Controller

The Stimulus controller is responsible for setting up the ActionCable connection, and then inserting the HTML received over the wire into the page.

The Javascript for commit_tags_controller.js, based off of a previous example, is:

import { Controller } from "stimulus"
import createChannel from "cables/cable";

export default class extends Controller {
  static targets = ['tags']

  connect() {
    let thisController = this;
    createChannel( { channel: 'CommitTagsChannel', request_id: this.data.get('request') }, {
      received({ tags }) {
        thisController.tagsTarget.innerHTML = tags;
      }
    });
  }
}

A second controller comes over the wire. It takes a selected tag and pulls in data from a JSON API and templates it.

The HTML and Controller

This controller generates the request_id as a UUID using SecureRandom. The HTML of the page starts with a single Stimulus controller, and a loading spinner.

<div data-controller="commit-tags" data-commit-tags-request="<%= @request_id %>">
  <h1 class="title">Latest Rails Tags</h1>
  <div data-target="commit-tags.tags">
    <div class="columns">
      <div class="column">
        Refreshing Tags...
        <svg width="44px" height="44px" viewBox="0 0 44 44" class="spinner show">
          <circle fill="none" stroke-width="4" stroke-linecap="round" cx="22" cy="22" r="20" class="path">
          </circle>
        </svg>
      </div>
    </div>
  </div>
</div>

The _commits.html.erb partial takes the JSON response from Github. It loads each tag, and sets up the JSON templating Stimulus controller.

<div class="columns" data-controller="commits" data-commits-url="https://api.github.com/repos/rails/rails/commits?per_page=15&sha=">
  <div class="column is-one-third">
    <aside class="menu">
      <ul class="menu-list">
        <% tags.each do |tag| %>
          <li>
            <a data-action="commits#selectBranch" data-target="commits.option" data-tag-name="<%= tag['name'] %>"><%= tag['name'] %></a>
          </li>
        <% end %>
      </ul>
    </aside>
  </div>
  <div class="column content is-two-thirds">
    <p>rails/rails@<span data-target="commits.branch"></span></p>
    <ul data-target="commits.commits" class="content">
    </ul>
  </div>
</div>

Some More Practice

You can find all the code on Github at https://github.com/johnbeatty/actioncable_activejob_stimulus.

Here are two different practice scenarios to level you up:

I. Use a different API endpoint

Try this tutorial but with a different API endpoint.

II. Use a real life request identifier

Try this tutorial, but add in a different kind of mechanism for linking the background job with the client waiting for data.

Interactivity

This example shows the powerful tools provided by Rails that let you make interactive web apps, without a whole rewrite in the Javascript flavor of the month.

Comments or Questions? Let me know how the practice problems went on twitter @jpbeatty

Want To Learn More?

Try out some more of my Stimulus.js Tutorials.

Published in ruby on rails Stimulus JS Tutorial

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *