SPA with Vue.js and Laravel: Understanding Route Transition

SPA with Vue.js and Laravel: Understanding Route Transition

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.

What is 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:

  • canActivate — Can we activate (enter) this new page component?
  • canDeactivate — Can we deactivate (leave) the current page component?
  • activate — What should happen when the new page component is activated?
  • deactivate — What should happen before the current page component is deactivated?
  • data — What data should this page component have when we get on it?
  • canReuse — Do you need this component to be reused if possible?

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>.

How to implement those transition hooks?

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.

Validation hooks

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.

Activation hooks

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.

Reusable components

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.

Loading data

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

  1. Using transition.next(obj) — where obj is the data we want to set.
  2. Directly returning that obj — a shortcut for the previous one.
  3. Returning a promise — which will eventually call 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
}

$loadingRouteData

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>

The transition object

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:

  • transition.from — the route object of the source path (you can review a list of the properties this object contains from the documentation) .
  • transition.to — the route object of the target path.
  • transition.next() — to allow the transition to move on to the next step.
  • transition.abort([reason]) — to prevent the transition from continuing.
  • transition.redirect(path) — to prevent the transition from continuing and redirect to a different route.

That’s it!

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.

Vue Laravel
Taha Shashtari

About Taha Shashtari

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.