Trouble with unexpected fetch request when changing pages

Hello,
I’m getting familiar with Turbo (amazing project) and got into trouble with understanding what’s going on here and why it behaves as it does. I’ll show it on simple example.

Goal: span#my_element_id would get updated with random number on link click, URL shouldn’t change on click

<turbo-frame>
<p>
    <span id="my_element_id">unknown</span>
    <a href="/app/get-random-number">
        Randomize
    </a>
</p>
</turbo-frame>
<a href="/app/different-page">Go to other page</a>

/app/get-random-number returns response like this (with random number obv.):

<turbo-stream action="update" target="my_element_id"><template>5970</template></turbo-stream>

All works as expected in this event flow (I’m logging all the events to see the flow):

  • turbo:click (/app/get-random-number)
  • turbo:before-fetch-request
  • turbo:before-fetch-response
  • turbo:before-stream-render
  • (number is updated in UI)

Until I click on „Go to other page“ link.
What happens:

  • turbo:click (/app/different-page)
  • turbo:before-visit
  • turbo:before-fetch-request
  • turbo:visit (/app/different-page)
  • turbo:before-fetch-response
  • turbo:before-cache
  • turbo:load
  • turbo:before-fetch-request
  • turbo:before-fetch-response
  • turbo:before-stream-render
  • (number is updated in UI, I can see it)
  • I move to /app/different-page

My question: why is there a fetch() for /app/get-random-number before moving to different page?

General goal is: click on a link to do some action on the server, receive turbo-stream(s) with UI changes, update UI and that’s all. It works until I go to another page.

I tried adding data-turbo-action="replace" in the /app/get-random-number link but it had no effect.

What am I missing in the logic of the requests?

Any help appreciated, thank you.

Hi @dasimcz,

There are 2 things:

  • turbo-frame
  • turbo-stream

A turbo-frame is a container with an id and a turbo-stream target a turbo-frame with the same id.

Goal: span#my_element_id would get updated with random number on link click, URL shouldn’t change on click

You need a little bit of js: you just need to update the turbo-frame “src” attributes, it will perform a fetch automatically. You can either set the “src” initially and it will fetch at the page load without the needed click.

So, taking your example, your turbo-frame should look like this:

<p>
  <turbo-frame id="my_element_id" src="/app/get-random-number"">
    <span>unknown</span>
  </turbo-frame>

  <a href=">Randomize</a>
</p>

<a href="/app/different-page">Go to other page</a>

And, the turbo-stream should look like this:

<turbo-stream action="update" target="my_element_id">
  <template>
    <span>5970</span>
  </template>
</turbo-stream>

If you want/need to change the random number multiple time, you should “update” the frame else you can “replace” the frame.

PS:

  <turbo-frame id="random_number" src="/app/get-random-number">
    <div class="random-number random-number--loading">
      Loading content from a stream...
    </div>
  </turbo-frame>

Paired with:

  <turbo-stream target="random_number" action="update">
    <template>
      <div class="random-number">
        2048
      </div>      
    </template>
  </turbo-stream>

A cool thing is for example to just update the “src” of the turbo-frame and it will fetch another time a random number.

If you are using stimulusjs …

// get_random_number_controller.js
import { Controller } from "stimulus";

export default class extends Controller {
  static values = { url: String }  
  static targets = ["container"];

  update() {
    this.containerTarget.src = this.urlValue
  }
}

with

<div class="random-number" data-controller="get-random-number" data-get-random-number-url-value="/app/get-random-number">
  <turbo-frame id="random_number" src="/app/get-random-number" data-get-random-number-target="container">
    Loading ...
  </turbo-frame>

  <a data-action="click->get-random-number#update">Randomize</a>
</div>

with

<turbo-stream target="random_number" action="update">
  <template>
    2048
  </template>
</turbo-stream>

Hi @nodalailama, thank you for responding and helping me out!

I get what you write about „re-writing“ my code. Your solution is probably better and I intend to use Stimulus as well for other tasks so it can be used for this one as well.

But my question stands: why is there a fetch() for /app/get-random-number before moving to different page?

I still don’t get where my understanding of the Turbo’s concepts is wrong. The extra request is confusing me and I’d be super happy for anyone to shed light on it for me.

Hey @dasimcz,

Okay, I missed the point: just live blank the “src” attribute and it won’t fetch anything automatically.

A frame could be used for 3 things:

  • pre-fetch a result from an url at the start if src is not blank;
  • fetch some content when you want if you change the src;
  • as a container with the id by any turbo-stream result that point to this id.

Cordialement,

Thank you for help but it’s still about something different. Let me give you another example.

Let’s have a counter on the site. Simple number. And a link to add 1 to the counter. I want to click the link, update the counter on the server and see it updated in UI. All that without redirect or URL change.

My /app/index page code would look like:

<turbo-frame id="container_element">
    <span id="counter_element">{{ default_counter }}</span>
    <br>
    <a href="/app/counter-pluplus">Add 1 to counter</a>
</turbo-frame>
<a href="/app/different-page">Go to other page</a>

Server code flow is:

Route /app/counter-pluplus increments counter in DB and redirects to /app/index which generates the whole page template again and returns it.

Turbo correctly picks it up and updates <turbo-frame id="container_element"> with new HTML with updated counter. Great! I can do it again and again.

Now I click on Go to other page and move to different page. And what happens in the background? /app/counter-pluplus is called once again and counter in DB is incremented in the background unexpectedly again.

Why? Why is the request called again? Is it some Turbo caching the page before leaving? But why is the request fetched again? How should this be done correctly for the request not to be called again when moving to another page?

When you click the link, are you issuing a GET request, or are you sending a POST to create the new counter visit? Remember, GET should be idempotent – read only, in practical terms.

You also cannot guarantee that the visitor will not have installed some crazy link-prefetch “helper” in their browser.

Walter

It’s a simple GET request.

Remember, GET should be idempotent – read only, in practical terms

Good point. I’ll probably take that into account when designing the flow.

But it does not make it clear to me why the request is replayed once again when going to another page. Is there any reason related to your point? If so, I’d love to understand Turbo’s behavior properly. Thanks.

I think that the “back” is memoized on exit, not load. That’s my guess, anyway. I could be entirely wrong about that. I don’t know if Turbo does the same thing when the initial request was a POST.

Walter

Strangely, I have got this problem too.
It’s like when caching the page, before going to the link, it re-fetch the frame src.

Sorry I was near all wrong. Maybe the process of caching add the page to a document and then the turbo lib listen to this and re-fetch.

Yesterday I coded a whole set of possible scenarios for my case to figure out what’s going on and what’s the best approach. When I used just a link in Turbo Frame with GET request, it was always „replayed“ when I moved to another page (probably because of caching because when I tuned it off with <meta name="turbo-cache-control" content="no-cache"> it was working ok).

Ideal approach was to perform the request as POST and return either redirect or stream response. Results were identical in UI and no extra unexpected request was performed.

As the goal I had was to use simple link, I ended uop with something like this (inspired by something I found on this forum I think):

Counter: <span id="counter_element">{{ default_value_from_backend }}</span>
<br>
<a href="/app/add-to-counter" data-postify>
    Add 1 to counter
</a>

<script>
    $(document).on('click', '[data-postify]', function (e) {
        e.preventDefault();
        let url = $(this).attr('href');
        var form = document.createElement("form");
        form.action = url;
        form.method = "post";
        document.body.appendChild(form);
        form.requestSubmit();
        form.remove();
    })
</script>

Basically just turning GET into POST request. Few notes:

  • backend needs to accept both, as GET would be sent in case of JS failure
  • backend should ideally reply with either stream (if Turbo req is recognized by Accept header) or redirect (if not)
  • GitHub - javan/form-request-submit-polyfill is needed for older browsers
1 Like

@dasimcz your original post sounds like a bug. I’ve been able to replicate. Just posting here to confirm correct replication, I will also report to the github repo.

<!-- index.html -->
<html>
<head>
<script type="module">
  import hotwiredTurbo from 'https://cdn.skypack.dev/@hotwired/turbo';
</script>
</head>
<body>
<turbo-frame>
<p>
    <a href="/app/get-random-number">
        this link goes to a 404
    </a>
</p>
</turbo-frame>
<a href="other_page.html">Go to other page</a>
</body>
</html>
<!-- other_page.html -->
<html>
<head>
<script type="module">
  import hotwiredTurbo from 'https://cdn.skypack.dev/@hotwired/turbo';
</script>
</head>
<body>
other page!
</body>
</html>

To start a server, python -m SimpleHTTPServer.

To replicate:

  • Click the link inside the frame
  • Observe a visit was made to that link (fine)
  • Click the link outside the frame
  • Observe two visits. One to link inside frame (bad), one to link outside frame (fine).

1 Like

For the record, if you disable snapshots on the original page, the issue goes away. (The cost is that you no longer have snapshots so the back button will require a full reload.)

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

@dasimcz can you please check if Prevent infinite looping when loading frames by seanpdoyle · Pull Request #165 · hotwired/turbo · GitHub fixes the bug

Yes, I can confirm that current version from the linked PR really does fix it. Thank you for (correctly) reproducing the bug and linking to the PR. I’ll look forward for it to get merged.

You’re welcome! Hopefully that gets merged soon.

1 Like