Waiting for the result of a Navigation

When using router-link, Vue Router calls router.push to trigger a navigation. While the expected behavior for most links is to navigate a user to a new page, there are a few situations where users will remain on the same page:

  • Users are already on the page that they are trying to navigate to.
  • A navigation guard aborts the navigation by doing return false.
  • A new navigation guard takes place while the previous one not finished.
  • A navigation guard redirects somewhere else by returning a new location (e.g. return '/login').
  • A navigation guard throws an Error.

If we want to do something after a navigation is finished, we need a way to wait after calling router.push. Imagine we have a mobile menu that allows us to go to different pages and we only want to hide the menu once we have navigated to the new page, we might want to do something like this:

router.push('/my-profile')
this.isMenuOpen = false

But this will close the menu right away because navigations are asynchronous, we need to await the promise returned by router.push:

await router.push('/my-profile')
this.isMenuOpen = false

Now the menu will close once the navigation is finished but it will also close if the navigation was prevented. We need a way to detect if we actually changed the page we are on or not.

Detecting Navigation Failures

If a navigation is prevented, resulting in the user staying on the same page, the resolved value of the Promise returned by router.push will be a Navigation Failure. Otherwise, it will be a falsy value (usually undefined). This allows us to differentiate the case where we navigated away from where we are or not:

const navigationResult = await router.push('/my-profile')

if (navigationResult) {
  // navigation prevented
} else {
  // navigation succeeded (this includes the case of a redirection)
  this.isMenuOpen = false
}

Navigation Failures are Error instances with a few extra properties that gives us enough information to know what navigation was prevented and why. To check the nature of a navigation result, use the isNavigationFailure function:

import { NavigationFailureType, isNavigationFailure } from 'vue-router'

// trying to leave the editing page of an article without saving
const failure = await router.push('/articles/2')

if (isNavigationFailure(failure, NavigationFailureType.aborted)) {
  // show a small notification to the user
  showToast('You have unsaved changes, discard and leave anyway?')
}

TIP

If you omit the second parameter: isNavigationFailure(failure), it will only check if failure is a Navigation Failure.

Differentiating Navigation Failures

As we said at the beginning, there are different situations aborting a navigation, all of them resulting in different Navigation Failures. They can be differentiated using the isNavigationFailure and NavigationFailureType. There are three different types:

  • aborted: false was returned inside of a navigation guard to the navigation.
  • cancelled: A new navigation took place before the current navigation could finish. e.g. router.push was called while waiting inside of a navigation guard.
  • duplicated: The navigation was prevented because we are already at the target location.

All navigation failures expose to and from properties to reflect the current location as well as the target location for the navigation that failed:

// trying to access the admin page
router.push('/admin').then(failure => {
  if (isNavigationFailure(failure, NavigationFailureType.redirected)) {
    failure.to.path // '/admin'
    failure.from.path // '/'
  }
})

In all cases, to and from are normalized route locations.

Detecting Redirections

When returning a new location inside of a Navigation Guard, we are triggering a new navigation that overrides the ongoing one. Differently from other return values, a redirection doesn't prevent a navigation, it creates a new one. It is therefore checked differently, by reading the redirectedFrom property in a Route Location:

await router.push('/my-profile')
if (router.currentRoute.value.redirectedFrom) {
  // redirectedFrom is resolved route location like to and from in navigation
  // guards
}