You may have noticed that the Vue 3 version of Vue router (4.0) came with a few breaking changes from its predecessor. Most of the changes are easy to spot, and the migration process is very straightforward. However, a very obscure but important change is often overlooked that can lead to hard-to-debug behavior: All navigations are now always asynchronous.
If you’ve been wondering why query params in your URL are nowhere to be found in your setup method or created hooks, but they still appear on your template when you interpolate them, stick around because we’re about to dive into what’s going on in those situations.
All navigations are now always asynchronous
To explore this, we’ll use a barebones Vue 3 skeleton app with Vue router 4.0 already installed. You can follow along with the code in this repo.
Go ahead and npm install
if you haven’t done so already, and npm run serve
the project locally. When you first visit the localhost URL, you will see that we are attempting to output the contents of our URL’s query params onto the page directly.
If you now go ahead and add some query params to the URL, like so: <a href="http://localhost:8080/?param=1" target="blank">http://localhost:8080/?param=1</a>
and reload the page, the page will correctly reflect the newly added param.
Let’s take a look inside App.vue, ****where I’ve also added a created
hook into our component. You’ll see a console.log
line that is outputting the contents $router.query
like we are in the template. Let’s go ahead and run the experiment again with and without the query param like we did a minute ago.
You will notice that no matter how many params you add, or how many times you reload the page, the object that is being logged into the console is empty. Let’s unwrap this and then I will show you how we can very easily fix this behavior.
As I alluded to at the beginning, one of the often overlooked Vue Router 4 breaking changes is that all navigations are now always asynchronous. This becomes more obvious when working with transitions, as the documentation suggests, because it will trigger the animation as the Router goes from empty to populated by data.
The reason why we can see the param in the browser, but not in the console, is because Vue’s reactivity system is kicking in and updating the HTML with the Router’s query object as soon as it is available. Remember, it is now async. This happens so quickly that to us it seems like it was always there, which can be really confusing when working with query params in setup functions or lifecycle hooks like created()
.
Fixing the problem
Luckily the fix for this is very straightforward. We just need to go into main.js where we are mounting our application and wait until the Router is done loading and is ready to mount our app, so that all the Router content is immediately available to our code.
📃 main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
// Replace -> app.mount('#app')
router.isReady().then(() => {
app.mount('#app')
})
If you go back to the browser and add the params and reload, you will now correctly see in the console window that the params are now available to us in the mounted
hook. You can also try this with a setup
function if you prefer the composition API.
Wrapping up
I hope this article helped you get unstuck (and understand the why) on a hard-to-debug but easy-to-fix Vue Router problem. See you in the next one!