In some cases, we want a way to control how our app should behave when the user is moving from page to page. For instance, can the user enter this new page? Or can he even leave the current page? Or what data should this page receive when we get on it? Or how can we do some cleanups before we leave a page?
All of these questions and more can be answered simply by using Route Transition.
It's the way we control how the router should work when moving from path to path. Nothing more, nothing less.
The way we tell it so is by implementing the optional transition hooks it provides us with. Note that by “optional” I mean you don’t have to implement them all, just the ones you need.
Before we get into further details, here’s the list of the transition functions we have:
You may be wondering now why I’m saying a page component instead of just a page. It’s because every page in Vue is a component. Do you remember this code block from main.js
?
router.map({
'/': {
name: 'home',
component: HomeView
},
'category/:categoryId': {
name: 'category',
component: CategoryView
}
})
As you already know, this is how we tell it which component to display when the route is matched. And remember, this component is rendered where we have that outlet element <router-view>
.
We’ve already seen in the previous part how to implement the data
transition hook. So, it’s obvious that we implement them under our component’s route
object.
I think it’s easier to explain these hooks by grouping them by purpose.
They determine whether it’s possible to leave or enter a certain page. We have two hooks for that: canDeactivate
and canActivate
.
The easiest way we can determine that is just by returning a boolean from that hook — true
to allow for that transition and false
to disallow it.
Here’s an example:
route: {
canDeactivate () {
if (userHasSavedHisWork()) {
return true
}
return false
// Or simply return the function itself
// return userHasSavedHisWork()
}
}
But that’s not the only way we can implement it. In fact, this is a shortcut. Vue will replace the returned boolean with a call to either transition.next()
or transition.abort()
depending on whether we return true
or false
.
But where does this transition
object come from? Well, this is always passed to all hook functions when they are called. But you don’t have to always use it. In fact, those hook functions will behave differently if you specify that object in the arguments list — more on that later.
So this means, we can reimplement it using the transition
object like this:
route: {
canDeactivate (transition) {
if (userHasSavedHisWork()) {
transition.next()
}
transition.abort()
}
}
Another way you can implement it is by returning a Promise from that hook. If that Promise was resolved — resolve(true)
— transition.next()
will be called, whereas transition.abort(reason)
will be called if the promise was rejected — reject(reason)
.
transition.next()
and transition.abort()
aren’t the only methods we can call. Another useful one is transition.redirect()
, which accepts a path to redirect to. In most cases, it makes more sense to redirect to some error page instead of just preventing the current page from changing.
After the validation phase passes and we can move onto the next page, we get the chance to do any necessary preparations and cleanups by implementing the functions: activate
& deactivate
.
Those two functions get called when you’re about to enter or leave the page — before the next page is displayed or before the current page is left.
activate
hook is usually used for control timing, because the switching won’t start until this hook is resolved. On the other hand, deactivate
is usually used to do cleanups.
Although activate
can also be used to load and set data on the current page — by directly assigning the data property, for example, this.topics = data
— we prefer to use data
hook instead. The main reason is because data
hook is called every time the route is changed even if the current page component is reused, whereas activate
is only called when the page component is newly created.
Another hook function we have is canReuse
. This is used to determine whether a component can be reused if it’s shared between the two paths. The default value for this hook is true
.
If you’re wondering how a component can be shared between two paths, here’s an example. Imagine we have these two paths: /category/1/topics
and category/1/moderators
. Here we can see that category
is repeated in both paths, which means it can be reused when we move between them.
Note that these two example paths are using nested routes — this means we have two nested <router-view>
elements — and that’s where canReuse
will start to take effect.
That’s all you have to know about it; it’s unlikely you’ll ever need to use it.
We’ve already learned about loading data in the previous part, so it shouldn’t feel new to us.
One important thing to know about this hook is that it’s always called when the route is matched regardless whether the component is reused or not — unlike how activate
works.
So, the rule of the thumb is to always use data
instead of activate
for loading data.
We mainly have three ways to load data using the data
hook; regardless which one you use, the returned object should always be in this form:
{
a: value1,
b: value2
}
Where a
& b
are the names of the data properties in the current component. Behind the scenes, Vue will use this returned object in this way: component.$set('a', value1)
and component.$set('b', value2)
.
Now, let’s see what those three ways are (they are kinda similar):
transition.next(obj)
— where obj
is the data we want to set.obj
— a shortcut for the previous one.transition.next
or transition.abort
depending on whether the promise was resolved or not.The second option is what we can always use; not only because it’s the simplest one, but also because it’s a perfect use for performing parallel requests — I’m talking here about Promise.all
.
Here’s an example from vue-router documentation.
With Promise.all
:
route: {
data (transition) {
var userId = transition.to.params.userId
return Promise.all([
userService.get(userId),
postsService.getForUser(userId)
]).then(function (data) {
return {
user: data[0],
posts: data[1]
}
})
}
}
If you’re familiar with promises, you should already know that Promise.all
will be resolved only when all child promises are resolved.
But this feels a little cumbersome, doesn’t it? We can instead make it a lot simpler by returning an object from that hook which its keys are the data property names in the component, and the values are the promises — That’s what I already explained above in the second option.
So, it become something like this:
route: {
data: function (transition) {
var userId = transition.to.params.userId
return {
user: userService.get(userId),
post: postsService.getForUser(userId)
}
}
}
How cool is that?
What is even cooler is that you can mix between promises and static values in that returned object. For example:
return {
user: userService.get(userId), // promise
post: postsService.getForUser(userId), // promise
pageTitle: 'Profile' // not a promise
}
To make it easier on us, Vue provides us with a property to determine whether the data
hook is resolved or not; and that’s $loadingRouteData
. This is mainly used to display loading state for the entering component. Here’s an example:
<div class="view">
<div v-if="$loadingRouteData">Loading ...</div>
<div v-if="!$loadingRouteData">
<!-- Display the page with its data -->
</div>
</div>
Now, let’s finish up this part by quickly reviewing the transition object.
It should be clear to you now that this object is passed to all hooks. But what you may not know is that when this object is specified in the hook’s arguments, the way this hook is resolved will be different.
By different I mean, if the transition object is specified, the hook will not be resolved until transition.next
is called — which gives us the ability to resolve the hook asynchronously.
If that object is not specified, on the other hand, the hook will be resolved synchronously.
One exception to this rule is that if you return a promise from the hook, it won’t be resolved until that promise is resolved regardless whether we specified the transition object in the arguments.
This transition object contains three methods and two properties:
That’s route transition in a nutshell. I hope I made it easy to understand; questions are welcome if anything is unclear!
I think it’s enough for the theory; in the next part, we’ll continue from where we left off and get back to building our awesome app.
I'm a freelance web developer. Laravel & VueJS are my main tools these days and I love building stuff using them. I write constantly on this blog to share my knowledge and thoughts on things related to web development... Let's be friends on twitter.