Routing (Vue 3)

Basics

Mechanism

  • client-side routing with history API

  • in createApp, create a router as root component

  • router reads current URL and renders the corresponding component

Concepts

thinking of "mapping a URL to a component view"

Get Started with Vue Router

Installation

yarn add vue-router@4

https://next.router.vuejs.org/guide/#router-view

  • define a root component with <router-view>

  • define and import some route components

  • define routes:

    • array of route objects, each is: path map to component

  • create router instance with:

    • a history mode (see below)

    • route definition

  • create app with root component

  • app.use(routerInstance); app.mount('#app')

History Modes

Hash mode

createWebHashHistory()

  • Use # before actual URL (root / redirects to liks "http://localhost:3000/#/")

    • because whatever after "#" is never sent to server, it does not require special treatment on server, everything goes /

    • bad impact in SEO

HTML5 Mode (recommended)

createWebHistory()

  • URL looks normal

  • Server needs to catch-all (whatever url) to give the root page (single page)

  • no server side 404, use client-side catch-all route option to render a 404 page

Route Mapping

Static Mapping

const routes = [

{ path: '/', component: Home },

{ path: '/about', component: About },

Route Parameter (/users/:id)

{ path: '/users/:id', component: User }, // value available under $route.params (e.g. $route.params.id)

Regular Expression Catching (/:parameterName(regularExpression e.g. .*))

{ path: '/:parameterName(.*)*', name: 'NotFound', component: NotFound }, // param regexp (after colon ":"), use regular expression to catch, then continue match the rest

value is put under `$route.params.parameterName` as an array of path segments

{ path: '/user-:anotherName(.*)', component: UserGeneric },

Repeatable Parameters ("+", "*", "?" must separated by /, return as array)

Difference between regex "+","*" - must separate by /, return as array

"+" : 1 or more, "*" 0 or more, "?" 0 or 1

{ path: '/:chapters+' }, // /:chapters -> matches /one, /one/two, /one/two/three, etc

{ path: '/:chapters*' }, // /:chapters -> matches /, /one, /one/two, /one/two/three, etc

{ path: '/:chapters(\\d+)+' }, // only match numbers, /1, /1/2, etc

Named Routes (use name and parameter to resolve to a path instead of string)

{ path: '/user/:username', name: 'user', component: User }

To resolve (construct and refer) to a named route, must pass an object

<router-link :to="{ name: 'user', params: { username: 'erina' }}">

  • params: for repeatable parameters, must pass an array

Providing Component with Props

  • Boolean mode { path: '/user/:id', component: User, props: true }

    • route.params will be set as component props

    • for named view, set for each name: props: { default: true, sidebar: false }

  • Object mode: props: { newsletterPopup: false }

    • set the props key/value as is

  • Function mode: props: route => ({ query: route.query.q })

    • parameters: the route object

    • return: the props object (key/value pairs for props)

Route Meta Fields

{

path: 'new',

component: PostsNew,

// only authenticated users can create posts

meta: { requiresAuth: true }

},

Accessible by:

  • on route location

  • navigation guards

Meta shallow merging:

  • because of nesting, URL can match multiple route records (parent -> child)

  • all can be found in $route.matched, can examine individually

  • a shallow merged meta(s) is in $route.meta - merge from parent to child, so child over-rides parent. use for e.g. "requireAuth"

Typing meta field: see https://next.router.vuejs.org/guide/advanced/meta.html#typescript



Online Testing Tool

https://paths.esm.dev/

Accessing the matched route(s) : $route

$route:

  • matched - an array of all matched route records

  • meta - merge (shallow) of all meta fields, from parent to child (so child over-rides parent)

Layout / Nesting (nesting route, parallel-nested-and-named<router-view>)

Usage

  • layout component - components with one or more <route-view> provide layouts

  • router options - maps URL to a "fill out" of the <route-view>s with components

  • so think of a URL as a "screen configuration" + parameters (query, path parameter)

Basics

  • Defining route mapping:

    • a route option include "path" and "children"

    • "children" is an array of path options (like "routes" in root router)

      • in children path options, leading "/" is treated as root - just the same

      • otherwise as relative to already matched path

  • Rendering

    • "path" in parent route option map to a component that has <router-view>

    • the component further matched by a child route opton renders inside the nested <router-view>

Multiple (parallel) Named <router-view>

  • Defining :

    • a route option include "path" and "components"

    • "components" is an object:

      • property key is "name" of <router-view>

      • value is component

  • Rendering

    • Include multiple <router-view name="nameOfYourView"> in a (layout) component

    • "components" will fill multiple router-view with matching names

Nesting Routes and Nesting Views Combined

https://next.router.vuejs.org/guide/essentials/named-views.html#nested-named-views

Same as above, a children[] has an option {path:"somePath", components: {}}



Redirect and Alias

Basics:

  • redirect: user visits /home, the URL will be replaced by /, and then matched as /

  • alias: user visits /home, the URL remains /home, but it will be matched as if the user is visiting /

    • can be a string or an array of strings

Examples

{ path: '/home', redirect: '/' }

{ path: '/home', redirect: { name: 'homepage' } } // target a named route


// a URL mapping function, can use to do relative redirecting

{

path: '/search/:searchText',

redirect: to => {

// the function receives the route object as the argument

// we return a redirect path/location here.

return { path: '/search', query: { q: to.params.searchText } }

},

},


Notes

  • Navigation Guards are not applied to the route that redirects, only on its target

  • redirect:

    • normal route option with redirect does NOT has component - it goes to the target

    • nested route (has "children") with redirect has component (behaviour? test to see)

Handling Dynamic (route parameter) Mapping

  • When map to same component, it's reused (meaning some lifecycle event not triggered)

  • to handle change:

    • watch $route.param change

created() {

this.$watch(

() => this.$route.params,

(toParams, previousParams) => {

// react to route changes...

}

)

},

    • or use beforeRouteUpdate navigation guard

async beforeRouteUpdate(to, from) {

// react to route changes...

this.userData = await fetchUser(to.params.id)

},



Static Navigation (links)

Link

  • do NOT use <a href="/">

  • use <router-link to="/">Go to Home</router-link>

  • To pass location descriptor object for named route <router-link :to="...">

Programmatic Navigation (push, replace, go, forward, back,)

Basics

  • $router.push() - returns a Promise, normal navigation

  • $router.replace() - navigation without pushing a new history entry

  • $router.go() - accepts a number, backward / forward number of records, fail silently if there's not enough records

  • $router.forward() - same as $router.go(1)

  • $router.back() - same as $router.go(-1)

Navigation is asynchronous. If want to do something AFTER it's done, use await.

If want to learn about the result (which might be cancelled):

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

Location Descriptor Object

  • path - string - path (use either "path" or "name"+"params")

  • name - string - name (for named route)

  • query - object - ?key=value

  • hash - string - '#something'

  • replace - boolean - act as replace()

  • params - object - is IGNORED if "path" is provided

    • parameterName

    • parameterValue:

      • single string

      • array of strings (for repeatable parameters)

      • "" empty string for optional parameter

Examples

// literal string path

router.push('/users/eduardo')


// object with path

router.push({ path: '/users/eduardo' })


// named route with params to let the router build the url

router.push({ name: 'user', params: { username: 'eduardo' } })


// with query, resulting in /register?plan=private

router.push({ path: '/register', query: { plan: 'private' } })


// with hash, resulting in /about#team

router.push({ path: '/about', hash: '#team' })

Navigation Resolution Flow and Navigation Guards

The Full Navigation Resolution Flow

  1. Navigation triggered.

  2. Call beforeRouteLeave guards in deactivated components.

  3. Call global beforeEach guards.

  4. Call beforeRouteUpdate guards in reused components.

  5. Call beforeEnter in route configs.

  6. Resolve async route components.

  7. Call beforeRouteEnter in activated components.

  8. Call global beforeResolve guards.

  9. Navigation is confirmed.

  10. Call global afterEach hooks.

  11. DOM updates triggered.

  12. Call callbacks passed to next in beforeRouteEnter guards with instantiated instances.

Guard Basic

  • guard navigation by redirecting or cancelling it

  • ways to hook:

    • globally (register with router instance)

      • router.beforeEach((to, from, next)=>{}) - triggered on every navigation

      • router.beforeResolve((to)=>{}) - after all in-component guards and async route components resolved, before navigation is confirmed - do something right before user enters a page

      • router.afterEach((to, from, failure)=>{}) - after navigation

    • pre-route (register in route option)

    • in-component (register with component options)

      • beforeRouteEnter(to, from) {} // before route (that renders this component) is confirmed, no access to "this"

      • beforeRouteUpdate(to, from){} // when route has changed but component is reused in new route (i.e. params change), has access to "this"

      • beforeRouteLeave(to, from){} // when route is about to be navigated away, has access to "this"

Global Before

router.beforeEach((to, from) => {

// explicitly return false to cancel the navigation

return false

})

Global Before Resolve

router.beforeResolve(async to => {

action: the same

usage: do something right before user enters a page - however if user cannot enter the page for some other reason, not to

Global After

router.afterEach((to, from, failure) => { // do something })

Cannot affect nevigation

Useful for:

  • analytics

  • changing title

  • check navigation failure

Pre-route Guard

{

path: '/users/:id',

component: UserDetails,

beforeEnter: (to, from) => {

// reject the navigation

return false

},

},

  • Only trigger when entering the route (from a DIFFERENT route) - no triggering when params / query / hash changes

  • can pass an array of functions - will be called in turn

In Component Guards - beforeRouteEnter (to, from, next) {})

  • no access to "this"

  • can pass a callback to "next", the callback receive the component as first parameter next(vm => {})

In Component Guards - beforeRouteUpdate (to, from) {}

  • can access "this"

In Component Guards - beforeRouteLeave (to, from) {}

  • to prevent user from accidentally leaving a route with unsaved data

  • ask user, then return false to cancel


Data Fetching (discussion)

Fetching after navigation

  • fetch data in component's "created" hook

  • display a loading state

Fetch before navigation

  • fetch in "beforeRouteEnter" guard, only call "next" to set data when fetch is complete

  • user stay in previous view - display a progress bar

Composition API - how to work inside (without "this")

Accessing the Router and current Route inside setup

import { useRouter, useRoute } from 'vue-router'

// inside "setup"

const router = useRouter()

const route = useRoute()

The route object is reactive. Avoid watching the whole object, just the param expected to change.

Setup In-component Navigation Guards with Composition API

import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
// inside "setup
// same as beforeRouteLeave option with no access to `this`
onBeforeRouteLeave((to, from) => {})
// same as beforeRouteUpdate option with no access to `this`
onBeforeRouteUpdate(async (to, from) => {})

useLink - internal resolution behaviour of RouterLink

export declare function useLink(props: RouterLinkOptions): {

route: ComputedRef<RouteLocationNormalized & { href: string }>,

href: ComputedRef<string>,

isActive: ComputedRef<boolean>,

isExactActive: ComputedRef<boolean>,

navigate: (event?: MouseEvent) => Promise(NavigationFailure | void),

}

  • Accepts: prop object that can be passed to <router-link>

  • returns:

    • route: RouteLocationNormalized + href:string

    • href: string

    • isActive

    • isExactActive

    • navigate: (event?)=>Promise()



Transitions

ignored

Scroll Behavior (scroll-to-top or preserve)

Controll scrolling behaviour, needs browser support.

How:

  • provide router object (when creating) with function scrollBehavior (to, from, savedPosition) { // return position }

    • savedPosition: available if this is a popstate nevigation (browser back/forward)

    • return a ScrollToOptions object, scroll to

      • el: "#elementId" - or a CSS selector or DOM element

      • top: 0 - a number

      • left: 0 - a number

      • behavior: 'smooth' - if browser supports "scroll behavior"

    • return false - no scrolling

    • return the savedPosition: result in a native-like behaviour with back/forward buttons

    • return a Promise - can be used to delay scrolling

Lazy Loading Routes

// instead of static import, use dynamic import

// and do not use "defineAsyncComponent" (async component), that's for use inside component, not inside router

const UserDetails = () => import('./views/UserDetails')

const router = createRouter({

// ...

routes: [{ path: '/users/:id', component: UserDetails }],

})

  • instead of a component constructor, give a function that returns a Promise that resolves to the constructor

  • inside the function, use dynamic import

  • will only fetch it when entering the page for the first time

  • "In general, it's a good idea to always use dynamic imports for all your routes."

Webpack Grouping Components in the Same Chunk, see document


Extending <router-link>

See document for creating a component that extends <router-link>

Navigation Failure (navigation result)

Reason for Navigation Failures

  • already on the page

  • a guard cancelled the navigation

  • a new guard takes place while old one not finished

  • a guard redirects

  • a guard throws

How to

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

  • false value (undefined) - success

  • Error instance

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

if (isNavigationFailure(failure, NavigationFailureType.aborted)) {

// show a small notification to the user

showToast('You have unsaved changes, discard and leave anyway?')

}

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

API: https://next.router.vuejs.org/api/#navigationfailure

Detecting Redirections

if (router.currentRoute.value.redirectedFrom) {

Dynamic Routing (routes options are dynamic, add/remove on the fly)

Adding New Route

const removeRoute = router.addRoute(routeRecord) // return callback used to remove

  • only register, does not cause navigation (if the current location matches)

  • inside navigation guard, if want to trigger redirection, do NOT call replace(), return new location to redirect

Removing Routes

  • add a new route with same name - removes the old route first

  • call the callback

  • route.removeRoute() - remove by name

When route is removed, all aliases and children are removed

Adding Nested Routes

router.addRoute('admin', { path: 'settings', component: AdminSettings }) // pass name as first parameter, adds to the children

Looking at existing routes

  • router.hasRoute(): check if a route exists

  • router.getRoutes(): get an array with all the route records.

The Router Object & API


The Route Object

The "$route" object:

  • is reactive

    • avoid watching the whole object, just the param expected to change