Advanced Vue Router Features in Vue 3 Applications (Part 2)
In this second part of our guide, we'll delve into advanced features of Vue Router in a Vue 3 application using VitePress. We'll cover nested routes, route guards, lazy loading, redirects, and more. We'll continue building upon the existing movie app, providing detailed explanations, in-depth code examples with file paths, and highlighting important tips and pitfalls.
Nested Routes
Nested routes allow you to create routes within routes, enabling complex UI structures like layouts and nested components.
Step 1: Update Router Configuration
Modify your router/index.ts
to include nested routes.
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import Home from '../views/Home.vue';
import About from '../views/About.vue';
import MovieDetail from '../views/MovieDetail.vue';
import MovieCast from '../components/MovieCast.vue';
import MovieReviews from '../components/MovieReviews.vue';
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Home',
component: Home,
},
{
path: '/about',
name: 'About',
component: About,
},
{
path: '/movie/:id',
name: 'MovieDetail',
component: MovieDetail,
props: true,
children: [
{
path: 'cast',
name: 'MovieCast',
component: MovieCast,
},
{
path: 'reviews',
name: 'MovieReviews',
component: MovieReviews,
},
],
},
];
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;
Step 2: Create Nested Components
Create MovieCast.vue
and MovieReviews.vue
in the components
directory.
<template>
<div>
<h2>Cast of {{ movie.title }}</h2>
<ul>
<li v-for="member in cast" :key="member">{{ member }}</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute();
const movieId = route.params.id as string;
const casts = {
'1': ['Leonardo DiCaprio', 'Joseph Gordon-Levitt', 'Ellen Page'],
'2': ['Keanu Reeves', 'Laurence Fishburne', 'Carrie-Anne Moss'],
'3': ['Matthew McConaughey', 'Anne Hathaway', 'Jessica Chastain'],
};
const movies = [
{ id: '1', title: 'Inception' },
{ id: '2', title: 'The Matrix' },
{ id: '3', title: 'Interstellar' },
];
const movie = computed(() => movies.find((m) => m.id === movieId));
const cast = computed(() => casts[movieId]);
</script>
<template>
<div>
<h2>Reviews for {{ movie.title }}</h2>
<ul>
<li v-for="review in reviews" :key="review.id">
{{ review.content }}
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute();
const movieId = route.params.id as string;
const reviewsData = {
'1': [{ id: 1, content: 'Amazing movie!' }, { id: 2, content: 'Mind-blowing!' }],
'2': [{ id: 1, content: 'A classic.' }, { id: 2, content: 'Changed sci-fi forever.' }],
'3': [{ id: 1, content: 'A journey through space.' }, { id: 2, content: 'Thought-provoking.' }],
};
const movies = [
{ id: '1', title: 'Inception' },
{ id: '2', title: 'The Matrix' },
{ id: '3', title: 'Interstellar' },
];
const movie = computed(() => movies.find((m) => m.id === movieId));
const reviews = computed(() => reviewsData[movieId]);
</script>
Step 3: Update MovieDetail.vue
Modify MovieDetail.vue
to include <RouterView>
for nested routes and links to navigate to the child routes.
<template>
<div>
<h1>{{ movie.title }}</h1>
<p>{{ movie.description }}</p>
<nav>
<RouterLink :to="{ name: 'MovieCast', params: { id: movieId } }">Cast</RouterLink>
|
<RouterLink :to="{ name: 'MovieReviews', params: { id: movieId } }">Reviews</RouterLink>
</nav>
<RouterView />
<RouterLink to="/">Back to Home</RouterLink>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute();
const movieId = route.params.id as string;
const movies = [
{ id: '1', title: 'Inception', description: 'A mind-bending thriller.' },
{ id: '2', title: 'The Matrix', description: 'A cyberpunk classic.' },
{ id: '3', title: 'Interstellar', description: 'A journey through space and time.' },
];
const movie = computed(() => movies.find((m) => m.id === movieId));
if (!movie.value) {
// Handle movie not found
}
</script>
Why Use Nested Routes?
Nested routes allow you to render components inside other components when the route matches. This is useful for creating layouts where child components are displayed within a parent component.
Route Guards
Route guards are functions that are called before a route is entered or left. They allow you to control access to certain routes.
Step 1: Implement Global Navigation Guard
Add a global navigation guard in your router configuration.
router.beforeEach((to, from, next) => {
const isAuthenticated = false; // Replace with real authentication check
if (to.name !== 'Home' && !isAuthenticated) {
next({ name: 'Home' });
} else {
next();
}
});
Important
Ensure that your authentication logic is secure and cannot be bypassed. Avoid storing sensitive data on the client-side.
Step 2: Per-Route Guard
You can also define guards on individual routes.
{
path: '/admin',
name: 'Admin',
component: () => import('../views/Admin.vue'),
beforeEnter: (to, from, next) => {
const isAdmin = false; // Replace with real admin check
if (isAdmin) {
next();
} else {
next({ name: 'Home' });
}
},
}
Lazy Loading Routes
Lazy loading improves your app's performance by splitting the code into smaller chunks and loading them on-demand.
Step 1: Update Routes to Lazy Load Components
Modify your router configuration to use dynamic imports.
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Home',
component: () => import('../views/Home.vue'),
},
{
path: '/about',
name: 'About',
component: () => import('../views/About.vue'),
},
{
path: '/movie/:id',
name: 'MovieDetail',
component: () => import('../views/MovieDetail.vue'),
props: true,
children: [
{
path: 'cast',
name: 'MovieCast',
component: () => import('../components/MovieCast.vue'),
},
{
path: 'reviews',
name: 'MovieReviews',
component: () => import('../components/MovieReviews.vue'),
},
],
},
];
Benefits of Lazy Loading
Lazy loading reduces the initial bundle size, improving the app's load time. Components are loaded only when the user navigates to that route.
Redirects and Aliases
Redirects and aliases help manage URL changes and provide multiple paths to the same component.
Step 1: Add Redirects
{
path: '/home',
redirect: '/',
}
Step 2: Add Aliases
{
path: '/movie/:id',
name: 'MovieDetail',
component: () => import('../views/MovieDetail.vue'),
props: true,
alias: '/film/:id',
}
Now, accessing /film/1
will render the MovieDetail
component just like /movie/1
.
Catch-all Routes for 404 Pages
Handle undefined routes using a catch-all route.
Step 1: Add a Catch-all Route
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('../views/NotFound.vue'),
}
Step 2: Create NotFound.vue
<template>
<div>
<h1>404 - Page Not Found</h1>
<RouterLink to="/">Go Back Home</RouterLink>
</div>
</template>
<script setup>
// No script needed
</script>
TIP
Always place the catch-all route at the end of your routes array to prevent it from overriding other routes.
Scroll Behavior
Define how the scroll position should be maintained when navigating between routes.
Step 1: Add scrollBehavior
Function
Modify your router configuration.
const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition;
} else {
return { top: 0 };
}
},
});
Now, when navigating back to a previous route, the scroll position will be restored.
Route Transitions
Add transitions when navigating between routes to enhance user experience.
Step 1: Wrap <RouterView>
with <Transition>
Modify App.vue
.
<template>
<div id="app">
<nav>
<RouterLink to="/">Home</RouterLink>
|
<RouterLink to="/about">About</RouterLink>
</nav>
<Transition name="fade" mode="out-in">
<RouterView />
</Transition>
</div>
</template>
<script setup>
// No script needed
</script>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
TIP
The mode="out-in"
ensures that the current component transitions out before the new component transitions in.
Difference Between History and Hash Mode
Understanding the difference helps you choose the right mode for your application.
History Mode (createWebHistory
)
- Uses the HTML5 History API.
- Clean URLs without the hash (
#
). - Requires server-side configuration to handle fallback.
Hash Mode (createWebHashHistory
)
- Uses the hash (
#
) part of the URL. - Doesn't require server-side configuration.
- The hash is not sent to the server.
Do's and Don'ts
- Do use History mode if your server is configured to handle SPA routes.
- Don't use History mode without proper server configuration, as it may result in 404 errors when refreshing the page.
Switching to Hash Mode
If your server cannot handle history mode, switch to hash mode.
import { createRouter, createWebHashHistory } from 'vue-router';
const router = createRouter({
history: createWebHashHistory(),
routes,
});
Conclusion
In this part of the guide, you've:
- Implemented nested routes to create complex UI structures.
- Learned how to control access to routes using route guards.
- Improved app performance with lazy loading of routes.
- Handled redirects, aliases, and catch-all routes for better URL management.
- Customized scroll behavior and added route transitions for enhanced UX.
- Understood the difference between history and hash modes in Vue Router.
Congratulations! You've now mastered advanced Vue Router features in a Vue 3 application using VitePress.