A Counter to Track Your Habits

Progressive Application Development with Hotwire

This is the day when you fall in love with Hotwire. We're going to build a simple counter. But we're not going to build it once and be done with it. Instead, we'll build and progressively enhance it with all three frameworks in Hotwire: Turbo Drive, Turbo Frames, and Turbo Streams.

21 min read
💡
If you are interested in learning about Hotwire, check out my crash-course on Hotwire's Turbo framework.

If you're a web developer, you must have heard the terms Progressive Enhancement and Progressive Web Apps (PWAs). What about progressively building your web application, introducing advanced techniques only when needed?

I recently listened to an episode of the Full-Stack Radio (Building HEY with Hotwire), where David Heinemeier Hansson talked about progressive web application development using Hotwire:

Start with the absolute simplest. Don't even do Frames, don't even do anything. Do Turbo Drive to start, just build the thing with normal forms, normal everything, and when you run into issues like... this part is reset everytime I add new stuff. What's the smallest thing I could do to fix that? Alright, let's do a frame.

After some time, it's getting too complicated and you need more fidelity, or you need to send the updates live over websocket, change it to Turbo Streams. But you should always think like 'how can we start from the absolute baseline? and then progressively enhance your application one step at a time.'

Don't start out with the most expensive fireworks at the first minute. No, save that stuff. Keep your powder dry.

To paraphrase, Turbo Drive gives you the majority of the benefits out-of-box, without having to do anything. When you need more interactivity on your components, you introduce Turbo Frames, and finally, when you need to update multiple elements on your page in a single response, you introduce Turbo Streams.

This approach sounded really interesting, so I decided to give it a try by building and progressively enhancing a small UI component using all three Turbo frameworks: Drive, Frames, and Streams.

I am going to build a simple habit tracker, which lets you keep track of the number of days you've consistently followed a habit. You can increment the counter and also decrement it to reduce the count, all without writing a line of JavaScript. This is how it looks in action.

First, we'll build the basic, fully-functioning counter with Turbo Drive. Then, we'll improve it using Turbo Frames. Finally, we'll optimize it further with Turbo Streams. In fact, I also built the whole counter in pure JavaScript, using the Stimulus library, but decided to leave it out for another post.

💌
My goal for this article is to make you fall in love with Hotwire, and show how enjoyable it is to build web applications, especially when combined with Ruby on Rails.

If you need a brief overview of Hotwire, check out the following article:

A Brief Introduction to Hotwire
Hotwire, which stands for HTML Over the Wire, provides a different way to build modern web applications without using too much JavaScript. This article provides a quick introduction to Hotwire and its component frameworks, such as Turbo Drive, Frames, and Streams.

A word of caution before you get started. This article is more than 4,000 words long, so don't try to finish it in one sitting. Instead, try to learn one Turbo framework a day by reading the docs and completing the related section of this post.

You should be able to finish everything in 2-3 days. I hope that after reading this article, you'll have a much better understanding of Hotwire and when to use each of its component frameworks. If you get stuck at any point, feel free to email me any time and I will try my best to help you get unstuck.

What You'll Learn:

Does that sound good? Let's get started.

Setting Up the Rails Application

Let's create a new Rails app named daily-habits, and launch it using the bin/dev command. Note that I am using Tailwind CSS for styling the components.

$ rails new daily-habits --css=tailwind

$ cd daily-habits

$ bin/dev

We'll start by creating the core data model for our application: a Habit.

Step 1: Generate the Habit Model

The very first thing that we're going to do is to generate a model named Habit with two properties: name and count, indicating the name of the habit and how many days you've followed that habit.

$ bin/rails generate model habit name:string count:integer

After running this command, Rails will generate a few files for you, including a database migration file. Let's run the database migration, so a habits table is created in the database.

$ bin/rails db:migrate

Now open the Rails console and create a sample habit named 'Write Every Day', because writing is awesome and you all should write every day.

$ bin/rails console

> Habit.create(name: 'Write Every Day', count: 5)

That's it. Now we have some data to work with and display on the screen. Next, we'll set up the route where we'll access the above habit.

Step 2: Setup a Route to Show a Habit

Open the config/routes.rb file and add a new route to it.

Rails.application.routes.draw do
  resources :habits, only: [:show]
end

This route instructs Rails to call the show action method on the HabitsController class, when a user visits the path /habits/1. You can also access this route programmatically using the URL helper named habit_path, passing an instance of Habit model.

If you want to learn routing in detail, check out the following article.

Understanding the Rails Router: Why, What, and How
The router is the entry point of your Rails application. It acts as the gatekeeper for all incoming HTTP requests, inspecting and sending them to a controller action; even filtering and rejecting them if necessary. In this article, we’ll do a deep dive into the Rails Router to understand it better.

Now that we have a valid route to show the habit, we need a controller and action to handle the incoming request.

Step 3: Create a Controller and Action

Let's use a generator to generate a HabitsController class with a show action.

$ bin/rails generate controller habits show

It will generate quite a few files, but we're only interested in the controller class. Edit the show action on the HabitsController class to set up an instance of habit.

class HabitsController < ApplicationController
  def show
    @habit = Habit.first
  end
end

At this point, we have a habit to show in our views, which we'll tackle next.

Step 4: Create A View to Render the Habit

Before building the view to show a habit, we'll tweak the application layout that Rails generated for us, so we have a nice area in the middle of the page to work with.

Open the application.html.erb file under the views directory, and update the <main> tag to add the following classes.

<main class="mx-auto mt-28 w-96 p-5 border rounded">
  <%= yield %>
</main>

Don't worry if they look confusing. All we're doing is center-aligning the main tag (with mx-auto and w-96), adding some padding (p-5) and margin (mt-28), and wrapping it inside a rounded border. Makes sense?

Now that we have a sandboxed area to play with, let's add a view to display the habit. For the HabitsController#show action, Rails will assume the location of the view file to be under views/habits directory, in a file named show.html.erb.

Let's open that file and add the following code to it. To add some extra spice to our habit tracker, I've added the streak markers below the habit, similar to how GitHub shows your commit streak. This will motivate us to follow the habit every day.

<div class="text-center font-bold text-gray-700" id="habit-name">
  <%= @habit.name %>
</div>

<div class="mt-3 flex justify-center items-center space-x-5">
  <button class="btn bg-red-300 inline-block shadow-lg">-</button>
  <div class="text-4xl font-bold"><%= @habit.count %></div>
  <button class="btn bg-green-300 inline-block shadow-lg">+</button>
</div>

<div class="mt-3 p-2 flex justify-center space-x-1">
  <% @habit.count.times do %>
    <div class="inline-block border p-1 bg-green-400"></div>
  <% end %>
</div>
💡
WARNING: If you haven't used Tailwind, this HTML might hurt your eyes. This is normal. That's how I first felt when I saw Tailwind last year. Trust me, it won't take too long before you fall in love with it.

Note: Don't copy + paste any code. Type everything by hand. As you type each class, save and refresh the browser to see the changes taking effect. The bin/dev command we used to launch the web server also compiles the Tailwind class names to actual CSS, so you should instantly see the changes.

It's a magical experience. Soon, once you get the muscle memory for Tailwind class names, the development will become so much more productive and enjoyable.

One last thing: do you see the btn class on the <button> elements? It's not a Tailwind class. I added that so we could group the common styles for frequently used components and reuse them. To add it, we'll open the application.tailwind.css file and add the following code to it.

@tailwind base;
@tailwind components;
@tailwind utilities;

@layer components {
  .btn {
    @apply rounded-lg py-1 px-3 inline-block font-medium cursor-pointer shadow;
  }
}

The class names should be self-explanatory. If you don't understand something, refer to the Tailwind CSS documentation.

Alright, we've finished the basic setup for this project. At this point, we have scaffolded the counter with a model, routes, controller, and views. Life's good.

Now visit localhost:3000/habits/1, you should see our habit with the initial count.

first look of the counter
First Look

However, it's not working yet. Nothing happens when you click the buttons.

Let's fix it.

Build with Turbo Drive

The magic of Turbo Drive is that you don't even notice it. It's just there, in the background, making your navigation faster.

In this section, we'll make the counter work by updating the count when you press the buttons.

I highly recommend that you read the Turbo Drive documentation before reading this section. For a brief introduction, check out this article:

Turbo Drive Essentials
I spent the last night reading up on the Turbo Drive, a part of Hotwire front-end framework that Rails 7 ships with. The core value proposition of Hotwire is that all the logic lives on the server, and the browser deals just with the final HTML. This post tries to

In a nutshell, when you click a link or submit a form, Turbo Drive does the following:

  1. Prevent the browser from following the link,
  2. Change the browser URL using the History API,
  3. Request the new page using a fetch request
  4. Render the response HTML by replacing the current <body> element with the response and merging the <head> element’s content.

The JavaScript window and document objects as well as the <html> element persists from one rendering to the next.

The same goes for an HTML form. Turbo Drive converts Form submissions into fetch requests. Then it follows the redirect and renders the HTML response.

As a result, your browser doesn’t have to reload, and the app feels much faster.

We're going to use the buttons (embedded inside forms) to increment and decrement the counter. Whenever the user presses the buttons, it will submit the form to the backend, which will update the habit count, and redirect to the updated habit.

As mentioned earlier, Turbo Drive will follow the redirect and update the whole body with the new response, all without reloading the browser.

Enough talking. Let's write some code.

Step 1: Create Routes to Update the Counter

Let's create two new routes to handle the requests to increment and decrement the counter.

Rails.application.routes.draw do
  resources :habits, only: [:show] do
    member do
      post :plus
      post :minus
    end
  end
end

If you haven't seen the above syntax with member before, learn more about them here: Custom, Non-Resourceful Routes in Rails

The above route configuration will generate the following routes:

$ daily-habits git:(main) ✗ bin/rails routes -g habit

     Prefix Verb URI Pattern                 Controller#Action
 plus_habit POST /habits/:id/plus(.:format)  habits#plus
minus_habit POST /habits/:id/minus(.:format) habits#minus
      habit GET  /habits/:id(.:format)       habits#show

Since the prefix for the routes is plus_habit and minus_habit, we can generate the corresponding URLs with plus_habit_path and minus_habit_path, respectively.

Now that we have the routes for the button clicks, let's add the actions to update the habits.

Step 2: Add Controller Actions to Handle Requests

Let's add two actions named plus and minus to increment and decrement the habit count. Note that I'm using the before_action callback to set the habit before all actions. This eliminates the duplicated code to set the habit.

class HabitsController < ApplicationController
  before_action :set_habit

  def show    
  end

  def plus
    @habit.update(count: @habit.count + 1)
    redirect_to @habit
  end

  def minus
    @habit.update(count: @habit.count - 1)
    redirect_to @habit
  end

  private
    def set_habit
      @habit = Habit.find_by(id: params['id'])
    end
end

After updating the habit, we're redirecting the user to the habit, which renders the show action. It will fetch and render the updated habit. Makes sense?

Alright, we're getting closer to the working implementation. Let's update the view to use the URLs for the above routes.

Step 3: Update View to Make Requests

We're going to replace the plain button tags with the button_to helper method provided by Rails. In addition to making the code expressive, it also creates a form that is submitted to the given URL. Nothing else needs to change.

<div class="text-center font-bold text-gray-700" id="habit-name">
  <%= @habit.name %>
</div>

<div class="mt-3 flex justify-center items-center space-x-5">
  <%= button_to '-', minus_habit_path(@habit), class: 'btn bg-red-300 inline-block shadow-lg' %>
  <div class="text-4xl font-bold"><%= @habit.count %></div>
  <%= button_to '+', plus_habit_path(@habit), class: 'btn bg-green-300 inline-block shadow-lg' %>
</div>

<div class="mt-3 p-2 flex justify-center space-x-1">
  <% @habit.count.times do %>
    <div class="inline-block border p-1 bg-green-400"></div>
  <% end %>
</div>

As you can see above, whenever the user clicks the minus button, it makes an HTTP POST request to the minus_habit_path(@habit), which returns /habits/1/minus URL.

Upon receiving the request on the above endpoint, the minus controller action will decrement the habit count by one and redirect to the show_habit_path(@habit), which returns habits/1.

Finally, Turbo Drive will follow the redirect request by making the fetch request to habits/1. Upon receiving the request, the show controller action will fetch the updated habit from the database and render it.

💡
So far, this is pretty much standard Rails stuff. What's cool about Turbo Drive is that at no point the browser is fully reloading, like it would in a typical web application that's not using any SPA framework or making explicit AJAX requests.

The form submissions (and link clicks) are handled by the Turbo library. It also makes the subsequent redirect, and updates the body with the resulting response.

And we are done! The counter component should be fully functioning now. Reload the browser and click the buttons a few times. The habit count should update accordingly.

Step 4 (Optional): Disable Turbo Drive

Let's try this again, this time with Turbo Drive disabled. I want to prove to you that Turbo Drive is indeed making our application work like a single-page application.

In the app/javascript/application.js file, add the following code to disable Turbo Drive.

Turbo.session.drive = false

Now reload the browser and click on the counter buttons. Notice something different? Every time you update the counter, the browser reloads.

This is the magic of Turbo Drive, it can update your web pages without doing a full reload.

Okay, enable Turbo Drive again. The counter should still function as expected without reloading the browser. You could just deploy it to production and be on your way with the next feature, but we're going to improve it using Turbo Frames.

Improve? But what's there to improve, you ask? Isn't it already behaving like a SPA? Well, yes, it is. But still, it's doing a lot more work than needed. Let's see how.

Build with Turbo Frames

We've learned that Turbo Drive replaces the <body> element and merges the contents of <head> without reloading the browser. If you open the network tab and follow the redirect, you'll see that Rails renders the entire view for the show action.

network tab for turbo drive

It's not that big of a deal in our case, where there's a single component and not too much HTML. However, for most large applications, you'll have a whole lot of other components on the screen, such as a header, a sidebar, some banner image at the top, a comments section, a blog post, etc.

We don't want to re-render all of these parts on our website every time someone updates the counter. Agreed, Rails is only replacing the body, but still, there can be a lot of HTML to send over the wire. What if instead of sending everything, we could only send the updated counter component?

Turbo Frames let us fix this issue by only sending specific HTML that we need to update, and keeping everything else on the page as it is.

Once again, I encourage you to review the Turbo Frames documentation to learn the basics. In a nutshell,

Turbo Frames allow predefined parts of a page to be updated on request. Any links and forms inside a frame are captured, and the frame contents automatically updated after receiving a response.

Regardless of whether the server provides a full document, or just a fragment containing an updated version of the requested frame, only that particular frame will be extracted from the response to replace the existing content.

Sounds great, right? Let's see how to implement it.

Step 1: Wrap the Habit in a Turbo Frame

The very first thing we are going to do is to wrap the element that we want to update dynamically (i.e. the habit component), inside a <turbo-frame> tag and assign it a unique id.

<turbo-frame id="habit-1">
  ...
</turbo-frame>

To make this even more convenient, Rails provides a nice helper function via the turbo-rails gem, which is included in your project by default.

<%= turbo_frame_tag 'habit-1' do %>
  ...
<% end %>

What's more, you can even pass an object to the turbo_frame_tag function. Rails will call the dom_id function on that object to ensure a unique id. Check out the turbo-rails documentation to learn more about this helper.

Let's update the show.html.erb view to wrap its content inside a turbo frame.

<%= turbo_frame_tag @habit do %>
  <div class="text-center font-bold text-gray-700" id="habit-name">
    <%= @habit.name %>
  </div>

  <div class="mt-3 flex justify-center items-center space-x-5">
    <%= button_to '-', minus_habit_path(@habit), class: 'btn bg-red-300 inline-block shadow-lg' %>
    <div class="text-4xl font-bold"><%= @habit.count %></div>
    <%= button_to '+', plus_habit_path(@habit), class: 'btn bg-green-300 inline-block shadow-lg' %>
  </div>

  <div class="mt-3 p-2 flex justify-center space-x-1">
    <% @habit.count.times do %>
      <div class="inline-block border p-1 bg-green-400"></div>
    <% end %>
  </div>
<% end %>

If you reload the browser and inspect the HTML, you'll see the generated <turbo-frame> tag.

Now that we've wrapped our counter inside a Turbo Frame, any clicks or form submissions made inside that frame will be intercepted by Turbo. Then it will make the fetch request to get a response, extract the <turbo-frame> element with the matching id from the response, and replace the existing <turbo-frame> with the new content.

This process is very similar to Turbo Drive, but instead of replacing the whole body, we are only replacing the matching Turbo Frame, which is definitely an improvement.

Step 2: There's No Step 2

That's it. We're done. Reload the browser and check if the counter still works as expected.

When you click the buttons, Turbo will submit the form to the above actions, which will update the habit count and redirect to the habit. Then, Turbo will follow the redirect to the show action.

The show action returns the response HTML containing the updated habit, which is also wrapped inside a <turbo-frame> tag. Upon receiving the response, Turbo will extract the matching turbo frame and update the existing frame with it.

What's more, the response HTML is reduced to only the specific data needed to update the component. We're not sending the remaining HTML, including the <head> content anymore.

The whole process is kind of magical, isn't it?

Here's what goes on behind the scenes to make it work.

💡
For all requests inside a Turbo Frame (link clicks or form submissions), Turbo sets the "Turbo-Frame" header.

The turbo-rails gem checks if this header is present, and uses a different, minimal layout for the response, instead of the standard application.html.erb.

Since Turbo will extract only the matching turbo frame, you don't need the rest of the original layout. Learn More.

This solves the problem we discussed at the beginning of this section. If there're any heavy components on this page, they don't need to be updated.

At this point, we have a working, efficient counter implementation.

Can we still improve it?

Bob the Builder - Yes, we can!
yes, we can!

We are now going to use Turbo Streams to update specifically the parts of the component that actually need to change, without affecting the rest of it. Sounds good?

Let's see how to do it.

Build with Turbo Streams

Let's revisit our view template for the habit counter again. I've also removed the turbo_frame_tag, we won't need it anymore.

<div class="text-center font-bold text-gray-700" id="habit-name">
  <%= @habit.name %>
</div>

<div class="mt-3 flex justify-center items-center space-x-5">
  <%= button_to '-', minus_habit_path(@habit), class: 'btn bg-red-300 inline-block shadow-lg' %>
  <div class="text-4xl font-bold"><%= @habit.count %></div>
  <%= button_to '+', plus_habit_path(@habit), class: 'btn bg-green-300 inline-block shadow-lg' %>
</div>

<div class="mt-3 p-2 flex justify-center space-x-1">
  <% @habit.count.times do %>
    <div class="inline-block border p-1 bg-green-400"></div>
  <% end %>
</div>

The component is made up of four elements:

  1. The name of the habit
  2. The buttons to update the count
  3. The habit count
  4. The streak markers
four elements of the counter component

As things stand now, upon clicking the buttons, we're replacing the whole component, including all of the above four elements. However, the only elements that are changing are the habit count (#3) and the streak markers (#4).

It would be nice if we could specifically target those two elements while leaving the other two as they were.

In our small example, this is not a big deal at all, but you could imagine having a large component with a few heavy parts that are updated less frequently and a few parts that update quite often.

If we're replacing the whole component each time an element changes, we have to rebuild all the heavy components that didn't change, which is not very efficient. This problem is similar to our discussion on Turbo Frames, just scoped to the context inside the frame.

Consider another scenario. What if one of the elements that need to be updated after changing the count lies outside the scope of the frame? e.g. ringing a notification at the top-right corner? Turbo Frame can't modify it, as it can only target one frame element at a time.

Turbo Frames won't let us target and update multiple elements on the page.

Turbo Streams solve this problem. Once again, I encourage you to read the Turbo Streams documentation and then return to this section.

In a nutshell,

Turbo Streams deliver page changes as fragments of HTML wrapped in self-executing <turbo-stream> elements. Each stream element specifies an action together with a target ID to declare what should happen to the HTML inside it.

Turbo Streams let us target and update multiple elements on the page in a single request, which is pretty cool, if you think about it.

We are going to use Turbo Streams to update only the elements that change, namely the habit count and the streak markers.

Step 1: Add IDs to the Elements of Interest

To modify the elements on the page, we need to specify their IDs in the turbo stream response. In our case, we want to update the habit count and the green streak markers. So let's wrap those elements inside separate <div> tags with IDs.

<div class="text-center font-bold text-gray-700" id="habit-name">
  <%= @habit.name %>
</div>

<div class="mt-3 flex justify-center items-center space-x-5">
  <%= button_to '-', minus_habit_path(@habit), class: 'btn bg-red-300 inline-block shadow-lg' %>
  <div class="text-4xl font-bold">
    <div id="habit-count"><%= @habit.count %></div>
  </div>
  <%= button_to '+', plus_habit_path(@habit), class: 'btn bg-green-300 inline-block shadow-lg' %>
</div>

<div class="mt-3 p-2 flex justify-center space-x-1">
  <div id="habit-markers">
    <% @habit.count.times do %>
      <div class="inline-block border p-1 bg-green-400"></div>
    <% end %>
  </div>
</div>

Step 2: Extract Markers to a Partial

Reusing server-side templates is a major goal of Hotwire.

This step is optional, but it will let us reuse the partial to avoid duplication. You'll soon learn how.

Let's extract the streak markers to a partial named _habit_markers.html.erb in the app/views/habits directory.

<div id="habit-markers">
  <% habit.count.times do %>
    <div class="inline-block border p-1 bg-green-400"></div>
  <% end %>
</div>

This is the resulting show.html.erb view after extracting the partial:

<div class="text-center font-bold text-gray-700" id="habit-name">
  <%= @habit.name %>
</div>

<div class="mt-3 flex justify-center items-center space-x-5">
  <%= button_to '-', minus_habit_path(@habit), class: 'btn bg-red-300 inline-block shadow-lg' %>
  <div class="text-4xl font-bold">
    <div id="habit-count"><%= @habit.count %></div>
  </div>
  <%= button_to '+', plus_habit_path(@habit), class: 'btn bg-green-300 inline-block shadow-lg' %>
</div>

<div class="mt-3 p-2 flex justify-center space-x-1">
  <%= render 'habit_markers', habit: @habit %>
</div>

The reason we extracted the partial is we want to reuse it inside the turbo stream response.

Step 3: Render Template Containing Turbo Streams

The last step is to create and render a new template called result.turbo_stream.erb after updating the count.

💡
When submitting a <form> element, Turbo injects text/vnd.turbo-stream.html into the set of response formats in the request’s Accept header. It also knows to automatically attach <turbo-stream> elements when they arrive in response to <form> submissions that declare a MIME type of text/vnd.turbo-stream.html.
class HabitsController < ApplicationController

  # ...
  
  def plus
    @habit.update(count: @habit.count + 1)
    render :result
  end

  def minus
    @habit.update(count: @habit.count - 1)
    render :result
  end

  # ...
end

The result template contains two <turbo-stream> elements that replace the habit count and the streak markers, respectively.

<%# app/views/habits/result.turbo_stream.erb %>

<%= turbo_stream.replace 'habit-count' do %>
  <div id="habit-count"><%= @habit.count %></div>
<% end %>

<%= turbo_stream.replace 'habit-markers' do %>
  <%= render 'habit_markers', habit: @habit %>
<% end %>

Note: I'm using the turbo_stream.replace helper function provided by the turbo-rails gem. Check out the turbo-rails documentation for more details.

Reload the browser, and click the buttons a few times. The counter should be working as expected.

If you open the network tab, you'll notice that the response HTML is even smaller, only containing two <turbo-stream> tags.

network tab for streams

It is pure magic.

That's a wrap. As you saw, it's very easy to progressively enhance your web application as your needs grow.

💡
Turbo Drive gives you the majority of the benefits out-of-box, without having to do anything. When you need more interactivity on your components, you introduce Turbo Frames, and finally, when you need to update multiple elements on your page in a single response, you introduce Turbo Streams.

In fact, I also implemented the above counter in pure JavaScript using Stimulus, but this article is getting way too bigger, and I doubt any of you would be still interested in doing a deep dive into Stimulus right now. So I will leave it for the next article.

Here're some additional resources, if you want to learn more about Hotwire.

Resources


I hope you found this article useful and that you learned something new.

If you have any questions or feedback, didn't understand something, or found a mistake, please leave a comment below or send me an email. I look forward to hearing from you. If you get stuck at any point, feel free to email me and I'll try to help you get unstuck.

Please subscribe to my blog if you'd like to receive future articles directly in your email. If you're already a subscriber, thank you.