Turbo Drive
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeIt's day 9! Beautiful day 9 where we start to make our app shine. All the fundamentals are in place - AssetMapper, Tailwind & Stimulus - so today is... almost a victory lap. We're about to get a huge bang for our buck thanks to a library called Turbo.
Right now, our site, of course, has full page refreshes. Keep an eye on the logo in the address bar. When I click, everything is done with a full page refresh. That's fine. Never mind, that's not fine! I want our site to have a devastatingly great user experience.
Luckily, we have Turbo on our team: a JavaScript library forged from the depths of the internet, bent on destroying all full page refreshes. Watch on their site: you won't see any full page reloads as we navigate. And check out how fast that feels. It feels like a single page application, because, well, it is, it's just not one that we need to build with a frontend framework like React.
Installing Turbo
Like Stimulus, Symfony has a package that helps us work with this Turbo. Find your terminal, and run:
composer require symfony/ux-turbo
When that finishes, do:
git status
Like the other UX package, this modified controllers.json and importmap.php. In assets/controllers.json, it added two new controllers:
| { | |
| "controllers": { | |
| // ... lines 3 - 12 | |
| "@symfony/ux-turbo": { | |
| "turbo-core": { | |
| "enabled": true, | |
| "fetch": "eager" | |
| }, | |
| "mercure-turbo-stream": { | |
| "enabled": false, | |
| "fetch": "eager" | |
| } | |
| } | |
| }, | |
| // ... line 24 | |
| } |
The first is... kind of a fake controller. It loads and activates Turbo - you'll see what that does in a moment - but it's not a Stimulus controller that we'll ever use directly. The second controller is optional - we're not going to talk about it, and it's disabled by default.
The other change, in importmap.php is, no surprise: it added @hotwired/turbo:
| // ... lines 1 - 15 | |
| return [ | |
| // ... lines 17 - 36 | |
| '@hotwired/turbo' => [ | |
| 'version' => '7.3.0', | |
| ], | |
| ]; |
The result of this single command is amazing. When I refresh, watch the address bar: we're not going to see any more full page reloads! And everything feels super-duper fast. Uh, I love it. Even the forms! Click edit. Watch: this submits via AJAX. Or, if I go and create a new one, hit enter, that submits via AJAX. Our site just got transformed into a single page app with one command!
Turbo: What's the Catch?
You might be thinking:
This is too good to be true, Ryan. What's the catch?
Ok, there is a catch, but minor for new projects: your JavaScript must be written to work without full page refreshes. Historically, we've written our JavaScript to execute on page load... or run on document.ready. And those things just don't happen after the first page load. But as long as you have everything written in Stimulus, you're good.
For example: our celebrate controller: it doesn't matter how many pages I click around to, that just keeps on rolling.
If your app isn't ready for Turbo yet - because of the JavaScript problem - you can disable it. In app.js, import * as Turbo from '@hotwired/turbo'. Below, say Turbo.session.drive = false. I'm not going to do that... so I'll comment it out:
| import * as Turbo from '@hotwired/turbo'; | |
| // ... lines 2 - 5 | |
| //Turbo.session.drive = false; | |
| // ... lines 7 - 8 |
But why would I install Turbo... just to disable it? Because Turbo is actually three parts. The first is called Turbo Drive. That's the part that gives us free AJAX navigation on all link clicks and form submits. And that's what this disables.
But even if you're not ready for Drive, you can still use the two other parts: Turbo Frames and Turbo Streams. These are powerful and we'll spend a lot of time in this tutorial doing some wild things with them.
Preloading Links
Turbo Drive itself is pretty simple, but it does have a few other tricks up its sleeve. And they're constantly adding new things. For example, one feature is called preloading. To show this off, go into templates/base.html.twig. If you're ever on a page... and you're really sure that you know what link the user is going to click next, you can preload that.
For example, on the "voyages" link, add data-turbo-preload:
| // ... lines 1 - 14 | |
| <body class="bg-black text-white font-mono"> | |
| <div class="container mx-auto min-h-screen flex flex-col"> | |
| <header class="my-8 px-4"> | |
| <nav class="flex items-center justify-between mb-4"> | |
| <div class="flex items-center"> | |
| // ... lines 20 - 27 | |
| <a href="{{ path('app_voyage_index') }}" data-turbo-preload class="ml-6 hover:text-gray-400">Voyages</a> | |
| // ... line 29 | |
| </div> | |
| // ... lines 31 - 36 | |
| </nav> | |
| </header> | |
| // ... lines 39 - 48 | |
| </div> | |
| </body> | |
| </html> |
Refresh, inspect element, then go to network tools, XHR... and clear the filter. When I refresh, we immediately see an AJAX request made for the voyages page! Because of this, when we click this link, watch: it's going to be instant. Boom!
Use this only when you're quite sure what the next page will be. We don't want to trigger a bunch of unnecessary traffic to your site that won't be used.
Oh, and see these JavaScript errors? These come from Symfony's web debug toolbar and profiler. I'm not sure why... but it doesn't like the preloading. That's something we need to fix, but the preloading itself works fine. You can ignore these.
Back in the template, remove the data-turbo-preload... because we don't really know what page the user will click to next.
Today was great. With one library, we eliminated all full page reloads. What could be next? Tomorrow we'll talk about Turbo Frames: a way for us to create Ajax-loading "portions" of our page, without writing a single line of JavaScript.
16 Comments
I just did: composer require symfony/ux-turbo at my Easy Admin project.
My controllers.json was edited:
At my composer.json I have the "symfony/ux-turbo": "^2.16", version
My project does not have a importmap.php
But when I'm navigating through my admin I still get full-page reloads.
What did I miss?
Hey @Tim-V
Are you trying to use Turbo with EasyAdmin? I'm afraid EasyAdmin has not added support for Turbo yet, perhaps they will in the future.
Cheers!
Hi @MolloKhan,
Thanks for your reply. Yes I am trying to get Turbo Drive and Frames working at my Easy Admin project. So there is no way to get the Turbo Drive or Frames working?
I'm not saying it is impossible but basically, you'll have to modify EasyAdmin in a way that it can work with Turbo. That's something I've not done in the past, so I can't say how hard it might.
As a note, Turbo 8 (added in UX v2.15.0) includes "InstantClick" (https://turbo.hotwired.dev/handbook/drive#instantclick) which causes a pre-load like event (and additional XHR calls) on hover.
InstantClick also makes it impossible to click on session links in the XHR panel of the debug tool bar because it hovering over the link causes another XHR request, which then loads another line into list of XHR calls and moves the link out from under your mouse pointer.
To disable InstantClick globally by adding
<meta name="turbo-prefetch" content="false">to your<head>, but then you cannot enable prefetch at all. Instead, you can adddata-turbo-prefetch="false"to your<body>tag and then you can enable prefetch (as shown in this video) on a link-by-link basis.I believe this may be fixed now in the latest versions of Symfony - https://github.com/symfony/symfony/pull/54004
So, yay for the community!
Thank you for sharing it @Daryl
Hi !
If I click on the "previous" button of my browser (not the one coded in the app) it goes, well, to the real previous page, but not in the previous screen like expected. Is there any way to prevent the default behavior of the browser's previous button ?
Thank you :)
Hey @Laurent!
Can you tell me a bit more about the behavior you'd like? You said:
What do you mean by "previous page" vs "previous screen"?
In general, if the user clicks something (e.g. to open a modal or open a side drawer) and you want the "Back" button to "undo" that, then the click needs to "advance" the navigation. For example, if you're navigating a turbo frame, by default, those link clicks to not advance the navigation (and so they also do not change the URL). But in that case, you CAN make a frame "advance" the navigation if you want: https://symfonycasts.com/screencast/last-stack/data-tables#advancing-the-frame
Let me know if this helps!
Cheers!
Thank you Ryan, the "advance" feature was what I meant.
thank you for this tutorial, it's really nice :)
It's great that you can move between previously visited pages using browser navigation without downloading them again from the server,
but how to prevent this after logging out (for pages that should not be available to non-logged in users)?
Does Trubo provide any mechanisms for this?
Hey @Nataniel-Z
That's a great question. I believe a full page refresh may be necessary after login out (redirecting to somewhere else). Give it a try and let me know
Cheers!
When following this, only on the Voyages page (not the homepage or the Planets, I get this:
(link to image)
Seems to be somewhere in the ajax toolbar panel (in the symfony profiler bar)
Yo @Joris-Mak!
I'm not sure... other than I know that the web debug toolbar, sometimes, gets confused by Turbo. I mean, I know this happens when you add the preload attribute - but I haven't seen it without that preload (which I remove at the end of the chapter).
Either way - no big deal - just the WDT getting confused.
Cheers!
I don't get this - why when being on "voyage" page preload the same "voyage" page?
Hey @Nataniel-Z!
That's just me showing a bad example. I didn't even realize that I was already on the voyages page when I did this. The point is: if you know what the NEXT URL will be (which, if used correctly, would never be the current URL), you can preload it. Good example of this might be some linear process or documentation where users tend to go from one page to another - not something we really have in our app.
Cheers!
"Houston: no signs of life"
Start the conversation!