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
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
Navigation triggered.
Call beforeRouteLeave guards in deactivated components.
Call global beforeEach guards.
Call beforeRouteUpdate guards in reused components.
Call beforeEnter in route configs.
Resolve async route components.
Call beforeRouteEnter in activated components.
Call global beforeResolve guards.
Navigation is confirmed.
Call global afterEach hooks.
DOM updates triggered.
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
})
action:
may return false to cancel navigation
may return RouteLocationRaw https://next.router.vuejs.org/api/#routelocationnormalized to redirect
throw Error - cancel navigation and call router.onError() callback
parameters: RouteLocationNormalized https://next.router.vuejs.org/api/#routelocationnormalized
may resolve asynchonously - navigation will be pending before resolve
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