Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Reloading When JS/CSS Changes

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.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

How does Turbo handle when a JavaScript or CSS file that's downloaded onto our page changes? When we navigate, it's smart enough to merge any new CSS or JS into our head element without duplicating anything that's already there.

But what about a CSS or JavaScript file whose contents just updated because we deployed? This is really a problem specific to production because locally, if we change a CSS or JS file in our editor, we just come back and trigger a full page reload manually. But how is this handled on production?

Well... if you do nothing, it's pretty simple: your users will continue to surf around with the old CSS and JavaScript... which is not something we want... especially since they will be getting the newest HTML from our site... which may only work with the newest CSS and JavaScript.

Activating Asset Versioning

But a slightly different thing happens if we enable versioning on our assets. Head to your editor and open up webpack.config.js. About halfway down this file... you'll find enableVersioning().

This tells Encore that, if we are doing a production build, each filename should contain a hash that's unique to its contents. It's a great strategy to make sure that when you deploy updates, each file gets a new file name... which forces users - in a non Turbo universe - to download the latest version.

To see what happens with Turbo, let's activate this for dev builds also by removing the Encore.isProduction() argument.

... lines 1 - 44
// enables hashed filenames (e.g. app.abc123.css)
.enableVersioning()
... lines 47 - 76

To make this take effect, find your terminal, go to the tab that's running Encore, hit Control+C and then rerun:

yarn watch

When that finishes... move over, refresh, and navigate around. If you check out the head tag, we have versioned filenames! The app.css file is now app.blahblah.css, and the app.js file also has a hash.

Let's go modify the app.js file - that's over at assets/app.js. At the bottom, console.log('new code').

17 lines assets/app.js
... lines 1 - 14
console.log('new code!');

Now, without refreshing your browser, navigate to a new page.. and look at the console. Interesting... no log! And we have two app.js script tags on the page... which is probably not what we want.

First, the new file wasn't executed because Webpack was smart enough to realize that the app entry script has already been loaded. So even though the script tag was added... and downloaded, Webpack prevented it from running: it knows that something weird is going on.

And even if it did load, it would probably mean that we would have things like event listeners registered twice on the page... which is also not what we want.

What we see in the head tag at least does make sense based on what we know about Turbo. Because the app.js has a new filename, it looks like a new script file. And so, Turbo added it to the head.

Refreshing the Page with data-turbo-track="reload"

So... how do we fix this mess? Well, let's think. One of the huge benefits of Turbo is that your JavaScript and CSS are downloaded and executed just once on initial page load... and then are reused for every navigation after. It's a big reason why Turbo is so fast. But if one of these files changes... we sort of do need to hit the "reset" button. In other words, this is one case when the page should do a full page reload so that our browser can download everything new.

Fortunately, there's an easy way to do this: by adding a special data-turbo-track attribute to every CSS and JS tag. And, it turns out, adding that attribute is super easy!

Open config/packages/webpack_encore.yaml. The script_attributes key allows us to add an attribute to every script tag that Encore outputs. Add data-turbo-track and set it to reload. We'll talk about what this does in a second. Also uncomment link_attributes and set the same thing here.

... lines 1 - 6
# Set attributes that will be rendered on all script and link tags
script_attributes:
defer: true
'data-turbo-track': reload
link_attributes:
'data-turbo-track': reload
... lines 13 - 33

With this simple change, every script and link tag that Encore renders will now have that data-turbo-track="reload" attribute on it.

So here's how this works... it's pretty simple. When we navigate, Turbo finds all of the elements with data-turbo-track on the current page and compares their filenames to the data-turbo-track elements on the new page. If the total collection of filenames on the old page does not exactly match the total collection of filenames on the new page, Turbo will trigger a full page reload.

Watch: if we click around, we see a lot of nice, boring Turbo-powered visits. But now go back to assets/app.js and remove that console.log().

Behind the scenes, a new app.js file with a new filename was just output. You can see it in the Encore terminal: before the filename was this, now the filename is different.

Back at our browser, let's visit a new page. Watch carefully. Yes! That was a full page reload! Turbo saw that the new page's "tracked" script and link tag filenames did not exactly match the old page's tracked filenames, and so, it triggered a normal, full-page-reload navigation. Problem solved!

Next: sometimes you may need to navigate to another page via custom JavaScript code. Like, maybe you have some custom JavaScript... where, after an Ajax call, you want to redirect to another URL. Could we use Turbo to do that visit instead of triggering a full page reload? Absolutely.

Leave a comment!

6
Login or Register to join the conversation
Ewald V. Avatar

Simply out of curiosity, why isn't the behavior behind the data-turbo-track="reload" attribute something that's done by default? Isn't this something you always want checked? Why is it optional?

Reply

Hey Ewald V.!

That's... actually a great question! There are two parts to this:

A) In Turbo, they need to know which things we consider as "assets" that should be tracked. So, they make us add this attribute. Maybe they could safely "guess" and use all link & script tags instead of forcing us to mark them. I'm not sure... there is likely an edge case where that wouldn't work.

B) In WebpackEncoreBundle, we could ALSO just "turn this on by default". We actually talked about this. I was against it, only because it feels weird to me to activate this data-turbo-track="reload" on every Symfony site by default regardless of whether they're using Turbo or no.

So, the end result is that this is something you need to activate... even though you always need it if you're using Turbo. We did update the recipe to have these lines, however. So you just need to uncomment them: https://github.com/symfony/...

Cheers!

1 Reply
Ewald V. Avatar

Clear! Thanks for the detailed answer!

Reply
Vafilor Avatar
Vafilor Avatar Vafilor | posted 1 year ago

Hey guys,

Does this mean you can't have multiple entries in your webpack.config.js file, combined with adding extra javascript and link tags per page when this is on? E.g.


{# templates/another/index.html.twig #}
{% block javascripts %}
{{ parent() }}

{{ encore_entry_script_tags('another') }}
{% endblock %}

If you do, and you visit another page, it will have new js/css, so it will always reload, right?

This is no problem if you're using all stimulus for javascript, but does this mean you would put all of your css into the main app.(s)css? Any tips for separation? I'm thinking of splitting things into partials and import them all in app.(s)css.

Thanks!

Reply
Vafilor Avatar

This does seem to be the case: https://discuss.hotwired.de...

Reply

Hey Vafilor!

Yes, as you already saw, this IS the case. The "reloading" system is kind of "dumb": if there are ANY new script/link tags then... boom! You get the full page reload.

Generally-speaking, I've been moving away from using multiple Webpack entry files. Instead, I'm using "dynamic imports" https://symfonycasts.com/sc... or the super-cool "lazy controllers" - https://symfonycasts.com/sc...

For me, this keeps my code simpler, but I still have the benefits of splitting things into smaller pieces to prevent having a GIANT app.js or app.css file. CSS can be dynamically imported in this exact same way.

> but does this mean you would put all of your css into the main app.(s)css?

I would put all of my "main" and most important CSS (for my general layout, etc) into this file (see note below). If I have some CSS that I'd like to not include in the final build app.css file for performance reasons, then I would try to import it from some Stimulus controller and load that controller lazily.

> Any tips for separation? I'm thinking of splitting things into partials and import them all in app.(s)css.

There are 2 reasons to separate your code: (A) performance (a smaller built app.css file) and (B) internal organization. I've talked a lot about part (A). But for (B), totally! Feel free to separate your main CSS into partials and then import from app.css - we do that internally here. Probably 90% of our CSS is done this way (they live in scss partials and are imported by app.scss). The last 10% are for features that are not on the main part of our site (e.g. challenges) and so we import them from a lazy stimulus controller.

Cheers!

1 Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=7.4.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "1.11.99.1", // 1.11.99.1
        "doctrine/annotations": "^1.0", // 1.13.1
        "doctrine/doctrine-bundle": "^2.2", // 2.3.2
        "doctrine/orm": "^2.8", // 2.9.1
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "sensio/framework-extra-bundle": "^6.1", // v6.1.4
        "symfony/asset": "5.3.*", // v5.3.0-RC1
        "symfony/console": "5.3.*", // v5.3.0-RC1
        "symfony/dotenv": "5.3.*", // v5.3.0-RC1
        "symfony/flex": "^1.3.1", // v1.18.5
        "symfony/form": "5.3.*", // v5.3.0-RC1
        "symfony/framework-bundle": "5.3.*", // v5.3.0-RC1
        "symfony/property-access": "5.3.*", // v5.3.0-RC1
        "symfony/property-info": "5.3.*", // v5.3.0-RC1
        "symfony/proxy-manager-bridge": "5.3.*", // v5.3.0-RC1
        "symfony/runtime": "5.3.*", // v5.3.0-RC1
        "symfony/security-bundle": "5.3.*", // v5.3.0-RC1
        "symfony/serializer": "5.3.*", // v5.3.0-RC1
        "symfony/twig-bundle": "5.3.*", // v5.3.0-RC1
        "symfony/ux-chartjs": "^1.1", // v1.3.0
        "symfony/ux-turbo": "^1.3", // v1.3.0
        "symfony/ux-turbo-mercure": "^1.3", // v1.3.0
        "symfony/validator": "5.3.*", // v5.3.0-RC1
        "symfony/webpack-encore-bundle": "^1.9", // v1.11.2
        "symfony/yaml": "5.3.*", // v5.3.0-RC1
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.1
        "twig/intl-extra": "^3.2", // v3.3.0
        "twig/string-extra": "^3.3", // v3.3.1
        "twig/twig": "^2.12|^3.0" // v3.3.2
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.0
        "symfony/debug-bundle": "^5.2", // v5.3.0-RC1
        "symfony/maker-bundle": "^1.27", // v1.31.1
        "symfony/monolog-bundle": "^3.0", // v3.7.0
        "symfony/stopwatch": "^5.2", // v5.3.0-RC1
        "symfony/var-dumper": "^5.2", // v5.3.0-RC1
        "symfony/web-profiler-bundle": "^5.2", // v5.3.0-RC1
        "zenstruck/foundry": "^1.10" // v1.10.0
    }
}