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.):
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.
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>
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.
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.
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.
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)
@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).
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.)
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.