Part 1: Turbo's Support for Redirects with Anchors

Turbo is a great tool, largely because it just works. However, anchors expose some very weird behaviour which will be covered in this post.
Chris Young
Chris Young
October 23, 2023

We love Turbo, so much that we've converted all of our applications early this year (2023). However, we still find there are some weird edge cases in working with it, just like there were with Turbolinks. Whenever possible we do our best to avoid disabling Turbo as it is the easy way out and sets a dangerous precedence.

In this post I'm going to review a problem we're facing and hopefully in a second post I'll share how we've worked around the problem. The issue is surprising in that it hasn't received more attention. The simple version is that anchors (fragments) behave unexpectedly.

Use Case

We have a typical notification pattern in all of our applications (using the noticed gem). A user has a list of notifications, when they click on it marks it as read and then redirects the user to the relevant place on the page.

More technically we can think of this as:

  1. User clicks link_to to view a notification.
  2. Controller marks the notification as read.
  3. Controller sends a redirect to the relevant location associated with the notification.

We make heavy use of anchors (fragments) to help users land on the relevant section of a page. For example, on a discussion page we use anchors to reference specific comments (via dom_id). This way a user can automatically scroll to the relevant comment within the context of a discussion. Or at least that is what we expect to happen!

Turbo Unexpected Behaviour

With Turbo disabled, the above scenario works beautifully. It is technically easy and makes sense from a user experience perspective. However, it doesn't work like this with Turbo!

One aside, some of our applications make use of multiple databases (RW / RO splits). We use the standard Rails multiplexing which basically says if it is a GET then send it to a RO instance, otherwise send it to a RW instance. Because of this, and because it is probably just the right pattern, we link to notifications via a PATCH method. This allows the database to use a write instance and properly mark the notification as read.

Consider the following frontend code that provides a link to mark a notification as read and then redirects the user to the related page:

<%= link_to "read notification", read_notification_path, data: { "turbo-method": :patch } %>

On the backend, we have a really simple controller method that does something like this:

class NotificationController < ApplicationController
  def read_notification
    # mark it as read

    # redirect to the related page
    redirect_to(URI.parse(@notification.to_notification.url).to_s, status: :see_other)

What would we expect to happen? Well, if the notification is for a comment with id 100, we might expect to go to a page at /discussion/1#comment_100 and nicely scroll us down to the area in the page where comment 100 is located.

This is how the non-Turbo world works!

However, speed isn't free. In the case above, on a brand new Rails 7.1.1 applications, the link will navigate but it completely loses the anchor and the user is left at the top of the page. Bummer.

We've run a number of tests and can confirm that this behaviour works the same way for Turbo GET requests as well.

If you are interested in checking out this odd behaviour yourself, we've created a minimal, Docker-friendly repo here that captures the state of things: harled/turbo-anchor-issue.

Even More Unexpected

To top off all of the weirdness, we did find a way to make it "work" with GET requests. Put the anchor tag on the link_to. That's right, just put the value on the frontend and watch it work! The value sent from the backend is completely ignored. The URL will noticeably flicker, the old anchor will be seen for a second, then the redirect will happen and the user will end up in the right place. They will have just gotten there in completely the wrong way.

Related Conversations

There are a number of conversations we're tracking in this space, however, we're mostly surprised that this isn't more of an issue. Folks must have found other patterns that work well ... although we shudder at the idea of re-educating our development team on how something as basic as anchors work.

Next Up

We believe that Turbo can be fast and not ask our development team to relearn anchors (not to mention we're about 13 points into a 2 point story because of this). We believe we've dug as deep as we can into Turbo, and it appears that the fetch() is to blame.

We have another area of an application where we have worked around this via query parameters. It feels dirty, but it does work. In Part 2 we hope to present either a solution where Turbo has a path forward, or a distance second option where we have consolidated on a pattern to avoid this issue.

Interested in joining a mission-driven team with a passion for Ruby on Rails? If so, please take a moment to look at our open positions!.

About the author

Chris Young

Chris is dedicated to driving meaningful change in the world through software. He has a history of taking projects from idea to production in fast yet measured ways. Chris has has experience delivering solutions to clients spanning fortune 100, not-for-profit and Government.