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

elements.png

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-streams-1.png

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.