Hotwire Discussion

Turbo drive stream updates, clicking browser back button does not change the page, only changes url

Hello, so today i’ve had this weird issue,

If you can see, when i click the browser back button the url changes but turbo does not replace the page again, when i inspect the network tab when clicking the browser back button no request is made to the backend to refetch the old page

Screenshot 2021-08-13 at 13 51 52

In the same page i have a :back button link that works like a charm!. So, i would appreciate any help. Thanks!.

Without seeing any code it’s difficult to say what’s wrong, but I have seen this behavior when controlling browser url routing in Javascript without using Turbo methods.

indeed. i have a stimulus controller that makes a GET form submit a turbo-stream request to the server.

import { Controller } from "stimulus";

export default class extends Controller {
  connect() {
    window.addEventListener(
      "turbo:before-fetch-request",
      this.appendTurboToHeaders
    );
  }

  disconnect() {
    window.removeEventListener(
      "turbo:before-fetch-request",
      this.appendTurboToHeaders
    );
  }

  appendTurboToHeaders(event) {
    let { headers } = event.detail.fetchOptions || {};

    if (headers) {
      headers.Accept = ["text/vnd.turbo-stream.html", headers.Accept].join(
        ", "
      );
    }
  }
}

for updating browser(and turbo) history i have another controller set up, which looks like this


import { Controller } from "stimulus";
import { Turbo } from "@hotwired/turbo-rails";

// this controller is used to manually update browser url without
// issuing any more requests to backend.
// you can attach data-action="turbo:submit-end@window->browser-url#change"
// and the controller will do the job for you.

export default class extends Controller {
  change(event) {
    if (event.detail.success) {
      const fetchResponse = event.detail.fetchResponse;
        history.pushState({}, null, fetchResponse.response.url);
        Turbo.navigator.history.push(fetchResponse.response.url);
    }
  }
}

What do you mean by Turbo methods @tleish, thanks for your answer!.

When using Turbo, pressing the back button requests in-memory cached pages from Turbo. Where this code attempts to manage history, I do not believe it updates Turbo cache. So the back button will not work as expected.

Do you have any ideas where i might start to tackle this issue?. I’ve found this Opting Out of Caching on the docs. by adding

 <meta name="turbo-cache-control" content="no-cache"

but i still get the weird behaviour when pressing backbutton. Thanks

@rockwell, did you have any luck solving this? I’m having much the same problem. The back button is displaying content which is obsolete. I should like the back button to trigger a request and update the contents of the page if anything’s out of date.

Hi @BKSpurgeon,

Could you explain what you want to acheive and share a little of your code (view + controller + stimulus) ?

Could you explain what you want to acheive and share a little of your code (view + controller + stimulus) ?

Sure, first I will share the basic code:

# tasks/index.html.erb
tasks_not_yet_done.each do |task|
	turbo_frame_tag task do
		link_to task_path(task), target: _top
	end
end
completed_tasks.each do |task|
	turbo_frame_tag task do
		link_to task_path(task), target: _top
	end
end
# tasks/show.html.erb
	turbo_frame_tag task do
		<div   data-controller=”mark_task_as_done”/>   
	end
// mark_as_done_controller.js is a stimulus controller 
// that sends a patch request to mark a task as “DONE” as soon as 
// the task is added to the dom.
  1. I’m on the task/index page. Suppose I click on a task that is not yet done.
  2. Turbo takes me to the tasks/15 page where task 15 is visible.
  3. The stimulus controller immediately marks the task as done! Great.
  4. When users hit the “back” button, the tasks/index.html page is stale – the task which should be marked as “done” (i.e. task 15) is still seen in the “tasks_not_yet_done” list. But really it should be in the “done” category.

The same pattern is seen everywhere: “Hey” have “two” lists of read and unread messages in their Imbox. When one clicks on a message and then hits the back button, that message which was originally unread is now categorised as being “read”. I want to do the same, but it’s not that easy trying to discern what basecamp arre doing from view source etc. Was seeking some general advise on how the community handles this problem.

https://turbo.hotwired.dev/handbook/building#understanding-caching

When navigating by history (via Restoration Visits), Turbo Drive will restore the page from cache without loading a fresh copy from the network, if possible.

I think, your problem come from this behavior.

yes no doubt. i’m trying to understand how Hey manages this bit. perhaps i will post it later if i discover it from the view source.

@BKSpurgeon, When you press the back button. Turbo will do a restoration visit that will fetch the page from cache(called preview) while simultaneously fetching the page from the server. That’s why you might see the flash between old and new state.

For these kinds of pages, what worked for me was putting

<meta name="turbo-cache-control" content="no-preview">

inside the <head>. In your case, you would put on the task index page. Let me know what happens

<meta name="turbo-cache-control" content="no-preview">

adding the above, and hitting the back button does not seem to be triggering any network requests. Not sure if this is the intended behaviour, or whether I am doing something crazy?

I would expect when I press the back button, as suggested, the cached result would temporarily display, and then the page fetched from the server would be rendered. I might have to do a demo app to look into this problem.

I think that you could listen to a click-item or before-turbo-send-request, when it fired then you changed the item from one list to another, then you continue the request, hit the server and go to the show item page… then you trigger back button, turbo show you the previous page and you item is in the right list.

1 Like

This is true for advance visits (clicking a link to advance the history), but not the case for restoration visits (clicking forward or back button on browser navigating history). For restoration (back/forward) visits, Turbo will show the cached version (if exists) and does not request an updated version.

2 Likes

@BKSpurgeon This controller really help me solve this issue. Basically i had a Multi step wizard. When progressing through the wizard everything was fine and was working. But, when the back button was pressed the content was not being replaced. I had a controller that was always present on the page during the various steps in the wizard. What i found out was. a) i had to listen to popstate events to listen for the back button press.

import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  static values = {
    widthClass: String,
    firstPage: Boolean,
  };

  initialize() {
    super.initialize();
    this.wizardStepChanged = this.wizardStepChanged.bind(this);
  }

  connect() {
    window.addEventListener("popstate", this.wizardStepChanged);
  }

  wizardStepChanged(event) {
    // when a user presses browser back-button inside a wizard step
    // turbo updates url but not the page since we are manually updating urls
    // when we return a stream response

    // when back-button pressed manually perform visit
    if (event.state.turbo_stream_response) {
      Turbo.visit(window.location.href, { action: "replace" });
    }
  }
}

This was successfully solving the back button press. Because i was manually telling Turbo to visit the url. For example, You are going from Page A to Page B. When in Page B, the user presses back button but it will not update the content, in that case the popstate listener will be invoked and will manually visit the url.

To take in account the first page, i also added this bit of code in the controller

import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  static values = {
    widthClass: String,
    firstPage: Boolean,
  };

  initialize() {
    super.initialize();
    this.wizardStepChanged = this.wizardStepChanged.bind(this);
  }

  connect() {
    if (this.firstPageValue) {
      this._changeHistoryStateToTurboStream();
    }

    window.addEventListener("popstate", this.wizardStepChanged);
  }

  wizardStepChanged(event) {
    // when a user presses browser back-button inside a wizard step
    // turbo updates url but not the page since we are manually updating urls
    // when we return a stream response

    // when back-button pressed manually perform visit
    if (event.state.turbo_stream_response) {
      Turbo.visit(window.location.href, { action: "replace" });
    }
  }

  _changeHistoryStateToTurboStream() {
    history.replaceState(
      { turbo_stream_response: true },
      window.location.title,
      window.location.href
    );
  }
}

Sorry if this does not make any sense. I should’ve noted better why i did what i did. I haven’t worked on it for about 2 months now. Hope this helps you in a way!.

1 Like