3
.
04
.
2024
3
.
04
.
2024
Ruby on Rails
Ruby
Hotwire
Frontend
Backend

Showing progress of background jobs with Turbo

Michał Łęcicki
Ruby Developer

Hotwire Turbo constantly changes the way we build backend-frontend integrations. Things that were complicated before, can now be done with a few lines of Ruby code. Let me present how to transmit progress information from background jobs into the frontend.

Tutorial application setup

For this article (and the next ones), I created the Chuck Norris Jokes Fetcher App®. We will use it to experiment and learn various Turbo features. The main functionality is basic: you create a request with a certain number of jokes to fetch. Then, the background job downloads them from Chuck Norris API.

Grab the link to repository and feel free to explore it. Tags are pointing to various stages of development.

The goal for today is to achieve this stunning progress bar:

Active Record broadcasts

Turbo provides an effortless way to broadcast any Active Record model updates to Turbo Streams. Therefore, we can bind the results of the background job with some Active Record model. It's intuitive and easy to implement, so let's see the code:

# app/models/joke.rb

class Joke < ApplicationRecord
  after_create_commit ->(joke) do
    broadcast_replace_to([ joke.jokes_request, "jokes_progress_bar" ],
                         target: "jokes_progress_bar",
                         partial: "jokes_requests/jokes_progress_bar",
                         locals: { jokes_request: joke.jokes_request })
  end

  belongs_to :jokes_request

  validates :body, presence: true
end

We use a callback to invoke broadcasting that replaces the existing progress bar partial with an updated one.

On the frontend side, we open a stream channel with the turbo_stream_from command. Its name must match the one from the callback: [ joke.jokes_request, "jokes_progress_bar" ].

# app/views/jokes_requests/show.html.erb

<%= turbo_stream_from @jokes_request, "jokes_progress_bar" %>

# (..)

<%= render "jokes_progress_bar", jokes_request: @jokes_request %>

Lastly, we need to render the partial with a progress bar:

# app/views/jokes_requests/_jokes_progress_bar.html.erb

<% progress_width = jokes_request.jokes.count / jokes_request.amount.to_f * 100 %>

<div id="jokes_progress_bar" class="w-full bg-gray-200 rounded-full">
  <div class="h-0.5 bg-lime-500 rounded-full" style="width: <%= progress_width.to_i %>%;"></div>
</div>

As you can see in the example, the solution requires minimal changes and works almost 'out of the box'. It has some drawbacks, though. Foremost: we introduced callbacks. Even though it's an officially recommended way, we don't like it. It quickly escalates, leading to "callbacks hell". Secondly, you can't always connect job results with creating records in the database. To deal with this issue, we could use an artificially created read model. But it's still not the best approach. Keep reading to see a more elegant solution.

Option 2: Direct broadcast from the job

Broadcasting to Turbo Streams doesn't necessarily need to be bound to Active Record. Turbo::StreamsChannel class can be used anywhere in the Rails application, so we can invoke it inside the worker/service:

# app/services/fetch_jokes_service.rb

class FetchJokesService
  def initialize(jokes_request_id)
    @jokes_request = JokesRequest.find(jokes_request_id)
  end

  # (...)

    def update_progress_bar(number)
      Turbo::StreamsChannel.broadcast_replace_to(
        [ jokes_request, "jokes_progress_bar" ],
        target: "jokes_progress_bar",
        partial: "jokes_requests/jokes_progress_bar",
        locals: { actual: number, limit: jokes_request.amount }
      )
    end
end

The broadcasting method mirrors the previous solution, with one noticeable difference: no Active Record dependency. We pass all input to the progress bar partial as separate variables.

# app/views/jokes_requests/show.html.erb

<%= turbo_stream_from @jokes_request, "jokes_progress_bar" %>

# (..)

<%= render "jokes_progress_bar", actual: @jokes_request.jokes.count,
                                 limit: @jokes_request.amount %>
# app/views/jokes_requests/_jokes_progress_bar.html.erb

<% progress_width = actual / limit.to_f * 100 %>

<div id="jokes_progress_bar" class="w-full bg-gray-200 rounded-full">
  <div class="h-0.5 bg-lime-500 rounded-full" style="width: <%= progress_width.to_i %>%;"></div>
</div>

We can use this approach to broadcast any other changes to the page: adding new joke elements, updating counters, etc. Even re-rendering pagination to ensure we are always displaying the proper page number!

Summary

Hotwire Turbo makes transmitting backend updates to the frontend a pleasure. Progress bars, counters, adding new elements, or even refreshing pagination can be written in Ruby, without touching any JavaScript! Hope this tutorial will help you in your Turbo adventures.

P.S. This joke caught me off guard: Chuck Norris does infinite loops in 4 seconds. 😂

Michał Łęcicki
Ruby Developer

Check my Twitter

Check my Linkedin

Did you like it? 

Sign up To VIsuality newsletter

READ ALSO

LLM Embeddings in Ruby - Paweł Strzałkowski

LLM Embeddings in Ruby

17
.
03
.
2024
Paweł Strzałkowski
Ruby
LLM
Embeddings
ChatGPT
Ollama
Handling Errors in Concurrent Ruby, Michał Łęcicki

Handling Errors in Concurrent Ruby

14
.
11
.
2023
Michał Łęcicki
Ruby
Ruby on Rails
Tutorial
Recap of Friendly.rb 2024 conference

Insights and Inspiration from Friendly.rb: A Ruby Conference Recap

02
.
10
.
2024
Kaja Witek
Conferences
Ruby on Rails

Covering indexes - Postgres Stories

14
.
11
.
2023
Jarosław Kowalewski
Ruby on Rails
Postgresql
Backend
Ula Sołogub - SQL Injection in Ruby on Rails

The Deadly Sins in RoR security - SQL Injection

14
.
11
.
2023
Urszula Sołogub
Backend
Ruby on Rails
Software
Michal - Highlights from Ruby Unconf 2024

Highlights from Ruby Unconf 2024

14
.
11
.
2023
Michał Łęcicki
Conferences
Visuality
Cezary Kłos - Optimizing Cloud Infrastructure by $40 000 Annually

Optimizing Cloud Infrastructure by $40 000 Annually

14
.
11
.
2023
Cezary Kłos
Backend
Ruby on Rails

Smooth Concurrent Updates with Hotwire Stimulus

14
.
11
.
2023
Michał Łęcicki
Hotwire
Ruby on Rails
Software
Tutorial

Freelancers vs Software house

02
.
10
.
2024
Michał Krochecki
Visuality
Business

Table partitioning in Rails, part 2 - Postgres Stories

14
.
11
.
2023
Jarosław Kowalewski
Backend
Postgresql
Ruby on Rails

N+1 in Ruby on Rails

14
.
11
.
2023
Katarzyna Melon-Markowska
Ruby on Rails
Ruby
Backend

Turbo Streams and current user

29
.
11
.
2023
Mateusz Bilski
Hotwire
Ruby on Rails
Backend
Frontend

Showing progress of background jobs with Turbo

14
.
11
.
2023
Michał Łęcicki
Ruby on Rails
Ruby
Hotwire
Frontend
Backend

Table partitioning in Rails, part 1 - Postgres Stories

14
.
11
.
2023
Jarosław Kowalewski
Postgresql
Backend
Ruby on Rails

Table partitioning types - Postgres Stories

14
.
11
.
2023
Jarosław Kowalewski
Postgresql
Backend

Indexing partitioned table - Postgres Stories

14
.
11
.
2023
Jarosław Kowalewski
Backend
Postgresql
SQL Views in Ruby on Rails

SQL views in Ruby on Rails

14
.
11
.
2023
Jan Grela
Backend
Ruby
Ruby on Rails
Postgresql
Design your bathroom in React

Design your bathroom in React

14
.
11
.
2023
Bartosz Bazański
Frontend
React
Lazy Attributes in Ruby - Krzysztof Wawer

Lazy attributes in Ruby

14
.
11
.
2023
Krzysztof Wawer
Ruby
Software

Exporting CSV files using COPY - Postgres Stories

14
.
11
.
2023
Jarosław Kowalewski
Postgresql
Ruby
Ruby on Rails
Michał Łęcicki - From Celluloid to Concurrent Ruby

From Celluloid to Concurrent Ruby: Practical Examples Of Multithreading Calls

14
.
11
.
2023
Michał Łęcicki
Backend
Ruby
Ruby on Rails
Software

Super Slide Me - Game Written in React

14
.
11
.
2023
Antoni Smoliński
Frontend
React
Jarek Kowalewski - ILIKE vs LIKE/LOWER - Postgres Stories

ILIKE vs LIKE/LOWER - Postgres Stories

14
.
11
.
2023
Jarosław Kowalewski
Ruby
Ruby on Rails
Postgresql