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.