Skip to content

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.

src/router/index.ts
ts
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.

src/components/MovieCast.vue
vue
<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>
src/components/MovieReviews.vue
vue
<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.

src/views/MovieDetail.vue
vue
<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.

src/router/index.ts
ts
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.

src/router/index.ts
ts
{
  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.

src/router/index.ts
ts
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 ​

src/router/index.ts
ts
{
  path: '/home',
  redirect: '/',
}

Step 2: Add Aliases ​

src/router/index.ts
ts
{
  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 ​

src/router/index.ts
ts
{
  path: '/:pathMatch(.*)*',
  name: 'NotFound',
  component: () => import('../views/NotFound.vue'),
}

Step 2: Create NotFound.vue ​

src/views/NotFound.vue
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.

src/router/index.ts
ts
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.

src/App.vue
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.

src/router/index.ts
ts
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.