Building with Hotwire

Previously, I outlined the new hotness that is Hotwire. New features are nice, but what can you build with it? Let's build a little project, and let's make it something different from the usual Twitter clone that's being used to demonstrate this new feature. Instead, I'll show how to build a basic scrolling price ticker like you'd see on any stock site that updates in real time. We'll learn a few interesting things along the way.

Setup

Presumably because it's still under development, Hotwire is not yet an included part of Rails 6. After creating a new project, you'll have to add it:

bundle add hotwire-rails
bundle exec rails hotwire:install

The bundle add command is a handy shortcut to update your Gemfile, courtesy of Rails 6. After doing this, you may start seeing errors about Trix not being defined. If so you'll have to install that as well, and it comes by way of Action Text:

bundle exec rails action_text:install

By default, Hotwire is configured to use Redis as a backend, so if you don't have that installed, you'll need to go ahead and do that as well.

Getting Data

Since everyone loves cryptocurrencies right now, let's populate our ticker tape with some quotes for that. First, we'll need a source of data. As you can probably imagine, these basically grow on trees at this point - you're spoiled for choice. For this project I'll use CoinAPI.io as my data provider. They give you a bit of free real time data, and their API is easy and straightforward to set up. Instead of using the standard library's HTTP classes, which can be cumbersome to work with, we'll add Faraday to our app to make it easier to work with a REST API:

bundle add faraday

One of the first things to do is to create a model to hold Quote data. You'll see the real reason why we need this shortly:

class AddQuote < ActiveRecord::Migration[6.1]
  def change
    create_table :quotes do |t|
      t.string :quote
      t.string :ticker
      t.timestamps
    end
  end
end

No major surprises there. Next, we'll want a way to start polling our chosen data source to get continuous updates on the current prices. ActiveJob is the perfect thing for this. The exact way you poll the data will be different depending on your chosen source. Here's how I pull quotes using mine:

class QuoteFetchJob < ApplicationJob
  queue_as :default

  def perform(ticker)
    pair = "#{ticker}/USD"
    api_key = Rails.application.credentials.dig(:coinbase_api_key)
    res = Faraday.new("https://rest.coinapi.io/v1/exchangerate/#{pair}", headers: { 'X-CoinAPI-Key' => api_key }).get
    data = JSON.parse(res.body)

    target = Quote.find_or_create_by(ticker: ticker)
    target.update_attribute(:quote, data['rate'])
    self.class.set(wait: 30.seconds).perform_later(ticker)
  end
end

Broadcasting

One thing that probably jumps right out is that we're saving these quotes to the database, even though that isn't really strictly necessary. You'll notice the same thing in any of the other examples that you'll see around the web. This is because Hotwire extends ActiveRecord with methods to broadcast relevant changes to the host application. This seems to be the main mechanism for doing it, and overall, it seems to strongly prefer that you either broadcast updates from a model or render them directly in a controller. You can try using Turbo::StreamsChannel.broadcast_replace_to to send updates from any arbitrary location in your code - I tried to use it to send updates directly from the Job listed above - but I found that it gets difficult and brittle to do so. Your mileage may vary.

So, how do we broadcast these updates to the model? Easily:

class Quote < ApplicationRecord
  after_update_commit { broadcast_replace_to 'cryptoquotes' }
end

The broadcast_replace_to method is one of a family of related methods that can remove, append, or replace front end elements. They do this by broadcasting commands over ActionCable that tell the front end what to do with the content being sent along with the commands. To give an example:

<turbo-stream action="replace" target="quote_2">
  <template>
    <turbo-frame id="quote_2">
      <span class='ticker'>ETH: 1841.0768953623622</span>
    </turbo-frame>
  </template>
</turbo-stream>

This is the sort of thing you'll see streaming over your ActionCable websocket whenever you update a Quote record. A turbo-stream element specifying what to do wraps a template element that contains the content to perform the action with. Take note of that turbo-frame element, because that's what ties all of this together. When the frontend of your application receives a command like this, it'll be looking for a turbo-frame tag with a matching id as it's target. Here's the matching snippet for our frontend:

<%= turbo_frame_tag dom_id(quote) do %>
  <span class='ticker'><%= quote.ticker + ": " %><%= quote.quote %></span>
<% end %>

This partial is responsible for rendering an individual quote - there will be multiple copies of this markup within the scrolling ticker, each with it's own HTML id that matches it's ActiveRecord id. This is a commonly used convention for doing Hotwire actions, but it's not strictly necessary - the HTML id can be anything you want or need it to be. When the model broadcasts the command, it will seek out the matching markup to update it. The markup shown above lives in a _quotes.html.erb partial, which can easily (and automatically) be rendered by simply calling render @quotes from our controller.

we'll embed the rendering of our partial within an index page. That page looks like this:

<h1>Hot Crypto News!</h1>

<%= turbo_stream_from "cryptoquotes" %>

<div class='marquee'>
  <%= render @quotes %>
</div>

Pay attention to the turbo_stream_from tag - that's your way of letting Turbo on the client side know that it should start listening for updates when this particular page is loaded. The render @quotes is a streamlined way of rendering partials that has been introduced recently - that's what will render our _quote.html.erb partial, once for each element in @quotes.

Starting up

To start periodically updating our Quotes and streaming those updates to our frontend, we just have to start background jobs for each ticker of interest:

desc "Start polling CoinAPI for quotes of interest"
task :start_quotes => :environment do
  puts "Starting Quote Polling..."
  QuoteFetchJob.perform_later('BTC')
  QuoteFetchJob.perform_later('ETH')
  QuoteFetchJob.perform_later('DOGE')
end

Or, if you're like me, you can just start the jobs from the console. As with any API, keep any applicable call limits in mind here - CoinAPI limits the number of free API calls you can make in a 24 hour period, for example. Add in the CSS of your choice to turn it into a horizontal scroller, and there you have it - a simple stock ticker.

This is by no means an exhaustive exploration of what Hotwire can do. It's still early days, and the gem itself is not yet completely solidified. As time goes by and more developers become familiar with it, we'll start to see just what it can do. Join in the fun and take Hotwire for a spin as soon as you get a chance!