How to opt out of the cache with Hotwire's Turbo Drive in Rails

| · @kyrylosilin · bluesky:@kyrylo.org

When using Turbo Drive and clicking a link, sometimes you don’t want that link to be pushed onto the navigation stack. Modals, slideovers, and temporary screens require this kind of behavior. With just a few lines of code, Turbo Drive makes this a reality.

In Matcharoo, when I start a new game, the game state is loaded, and the UI changes its look to show the user that they transitioned to the game mode. Here we make a URL click:

When you complete a game, a Turbo.visit call happens through JavaScript. It loads the post-game statistics. The statistics are rendered by the server as a Turbo Frame.

You can click Continue, and another Turbo Frame will be loaded to show your current leaderboard position.

Then, you click Continue again and progress to the next level.

Problem

The problem appears when you press the back button when you have cleared a level. With normal navigation, you are redirected back to the game screen. This is unexpected because in the app, there’s a clear distinction between the normal and the game modes. You expect that going back will take you to the normal screen instead.

Solution

Turbo doesn’t know anything about your state. We need to help it understand. We are specifically interested in how Turbo deals with the cache.

Turbo caches every visit by default.

On the first screen, when we enter the game mode, we load the game URL (/games/new). We need to tell Turbo to exclude the game page from the cache.

Add the content_for?(:head) check to your layout to be able to add new tags to <head> from your views.

<%# app/views/layouts/application.html.erb %>

<% if content_for?(:head) %>
  <%= yield(:head) %>
<% end %>

Then, modify the template that loads the game state and add the turbo-cache-control directive.

<%# app/views/games/new.html.erb %>

<% content_for :head do %>
  <meta name="turbo-cache-control" content="no-cache">
<% end %>

Half of the job is already done!

This will prevent the issue that you can observe when you go back from the normal mode to the game mode after you have already completed one game (e.g., loaded that screen once). For a fraction of a second, you will see the old screen until the new one is loaded. We don’t want that because it makes the experience janky.

Here’s how it would look like without turbo-cache-control:

And here’s how it looks with no-cache in place:

We also no longer see the leaderboards screen when we navigate from the normal mode screen!

The no-cache value for the turbo-cache-control told Turbo Drive to ignore all the Turbo Frames we loaded while being on /games/new.

The final half of the job is to use turbo_action: "replace" when we leave the game mode.

The first call to Continue is a normal link. It loads leaderboards in a Turbo Frame:

<%= link_to "Continue", leaderboards_path(game) %>

However, the last call to Continue must be performed as a replace visit.

The replace visit action uses history.replaceState to discard the topmost history entry and replace it with the new location.

As we leave the game mode, we replace the /games/new URL with a new one, and it essentially disappears from the navigation stack. I also added turbo_frame: "_top" to break out of the game statistics Turbo Frame.

<%= link_to(
  "Continue",
  next_level_path(@game.level),
  data: {
    turbo_frame: "_top",
    turbo_action: "replace",
  }
) %>

Now we can go back to the previous level when we press the “Back” button. The game screen is no longer loaded.

Problem solved!

TL;DR. Use <meta name="turbo-cache-control" content="no-cache"> with turbo_action: "replace".

You can discuss this article on X/Twitter:
https://twitter.com/kyrylosilin/status/1749041960638165025

← Making PostgreSQL work with... · Home · How to enable the Traefik d... →