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.
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?
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.