Vue.js 3 Route Parameters & Nesting Tutorial

In this Vue tutorial we learn about route and query parameters.

We cover how to create these parameters and access the data in them as well as nested routes and page scroll behavior.

Lesson Project

If you want to follow along with the examples in this lesson, you will need an app generated by the Vue CLI with the Router package installed , as well as the following extra components.

  • src/router/index.js
  • src/views/Home.vue
  • src/views/Users.vue

The project should look similar to the following.

Example: project
project_folder/
├── src/
|   ├── components/
|   ├── router/
|   |   └── index.js
|   ├── views/
|   |   ├── Home.vue
|   |   └── Users.vue
|   └── App.vue

The /src/router/index.js file is where our routes are defined.

Example: src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      name: 'Home',
      component: () => import('../views/Home')
    },
    {
      path: '/user',
      name: 'Users',
      component: () => import('../views/Users')
    }
  ]
})

export default router

We link to the routes in the root App component and show the router-view below that.

Example: src/App.vue
<template>
  <div>
    <router-link :to="{ name: 'Home' }">Home</router-link> |
    <router-link :to="{ name: 'Users' }">Users</router-link>
  </div>

  <router-view/>
</template>

The Home and Users views each have a heading that allows us to easily identify them.

Example: src/views/Home.vue
<template>
  <h2>Home page</h2>
</template>
Example: src/views/Users.vue
<template>
  <h2>Users page</h2>
</template>

Route parameters

An application will often need to pass parameters through a route to a view.

As an example, let’s say we have a blog where users can log in and write articles. We will need to have some way of identifying each user, like an ID, so that we can create an Author Bio view.

We can do that by adding dynamic segments inside our paths. A dynamic segment can have any name we want, but it must be prefixed with a colon operator.

Syntax: dynamic segment
path: '/user/:segment_name'

To demonstrate, let’s create another view in /src/views/UserSingle.vue .

Example: project
project_folder/
├── src/
|   ├── components/
|   ├── router/
|   |   └── index.js
|   ├── views/
|   |   ├── Home.vue
|   |   ├── Users.vue
|   |   └── UserSingle.vue
|   └── App.vue

It’s template can just have a heading that identifies the page for now.

Example: src/views/UserSingle.vue
<template>
  <h2>Single user page</h2>
</template>

In /src/router/index.js , we’ll add a route to it with a segment called “id”.

Example: src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      name: 'Home',
      component: () => import('../views/Home')
    },
    {
      path: '/user',
      name: 'Users',
      component: () => import('../views/Users')
    },
    {
      path: '/user/:id',
      name: 'UserSingle',
      component: () => import('../views/userSingle')
    }
  ]
})

export default router

We can run the example and type anything in the browser’s url bar after /user/ and it will load the UserSingle view, like http://localhost:8080/user/john .

Route links with parameters

When we use routes with parameters, we can link to them by adding a second params key to the link object.

The params key takes an object with all the parameters we have in the route path as its value.

Syntax: router-link with params
<router-link
  :to="{
    name: 'routeName',
    params: {
      parameter: 'parameterValue'
    }
  }">
Link Text
</router-link>

The parameter must be the same one that we defined in the route, and the value is whatever will be in the URL.

To demonstrate, let’s add a parameterized link to our example in the Users view.

Example: src/views/Users.vue
<template>
  <h2>Users page</h2>

  <router-link
    :to="{
      name: 'UserSingle',
      params: {
        id: 12345
      }
    }">
    User with id 12345
  </router-link>
</template>

If we navigate to the Users page in the browser and click on the link, it will load the UserSingle component.

In a more realistic scenario, we’ll have users defined in an array that we loop over to create a link for each.

Example: src/views/Users.vue
<template>
  <h2>Users page</h2>

  <p v-for="user in users" :key="user.id">

    <router-link
      :to="{
        name: 'UserSingle',
        params: {
          id: user.id
        }
      }">
      ({{ user.id }}) {{ user.name }}
    </router-link>

  </p>
</template>

<script>
export default {
  data() {
    return {
      users: [
        { id: 1, name: 'John' },
        { id: 2, name: 'Jane' },
        { id: 3, name: 'Jack' },
        { id: 4, name: 'Jill' }
      ]
    }
  }
}
</script>

If we click on any of the links on Users page, it will load the UserSingle component.

Access a route parameter with the $route instance object

The Vue router gives us access to the $route instance object. On this object is the params property that we can use to access the parameter from the URL.

Syntax: $route.params
$route.params.propertyName

The property name has to be the same as the one we define in the route path.

As an example, to access the id parameter in the UserSingle view, we’ll add $route.params.id to a paragraph in the template block.

Example: src/views/UserSingle.vue
<template>
  <h2>Single User page</h2>
  <p>User id: {{ $route.params.id }}</p>
</template>

If we click on any of the users in the Users page, it will load up the UserSingle view and display the user’s id.

Parameters as props

We can tell Vue that we want to handle our parameters as props by setting the props option in the route definition to true.

Syntax: props
{
  path: '/path/:param',
  name: 'RouteName',
  component: View,
  props: true
}

To demonstrate, let’s set our UserSingle view to handle its parameters as props.

Example: src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      name: 'Home',
      component: () => import('../views/Home')
    },
    {
      path: '/user',
      name: 'Users',
      component: () => import('../views/Users')
    },
    {
      path: '/user/:id',
      name: 'UserSingle',
      component: () => import('../views/UserSingle'),
      // handle these as props
      props: true
    }
  ]
})

export default router

Vue will now pass the parameter to the UserSingle component as a prop, which means we can handle it as a prop instead of using the instance object.

Example: src/views/UserSingle.vue
<template>
  <h2>Single User page</h2>
  <p>The user id is: {{ id }}</p>
</template>

<script>
export default {
  props: ['id']
}
</script>

If we test each user in the browser, it will look exactly the same as before. But now we handle the data internally as a prop.

Query parameters

The Vue router also allows us to use query parameters in our routes. These are the key=value pairs that come after the ? in a url.

Example: query parameters
www.example.com/user/1?order=id&sort=asc

Query parameters are constructed just like route parameters, except we use the query option with an object that contains the key:value pair.

Example: query parameter
<router-link
  :to="{
    name: 'routeName',
    query: {
      query: 'queryValue'
    }
  }">
Link Text
</router-link>

As an example, let’s add a query parameter to our user links for the user’s name.

Example: src/views/Users.vue
<template>
  <h2>Users page</h2>

  <p v-for="user in users" :key="user.id">
    <router-link
      :to="{
        name: 'UserSingle',
        params: { id: user.id },
        // ?name=John
        query: { name: user.name }
      }">
      ({{ user.id }}) {{ user.name }}
    </router-link>
  </p><hr>

  <router-view/>
</template>

<script>
export default {
  data() {
    return {
      users: [
        { id: 1, name: 'John' },
        { id: 2, name: 'Jane' },
        { id: 3, name: 'Jack' },
        { id: 4, name: 'Jill' }
      ]
    }
  }
}
</script>

If we run the example in the browser and click on a user, the url will show that the query parameter has been added.

Example:
http://localhost:8080/user/2?name=Jane

Multiple parameters are separated with a comma. In the url they will be separated with an & symbol.

Example: multiple query parameters
// the following query
query: { name: user.name, foo: 'bar' }

// creates the following url
http://localhost:8080/user/2?name=Jane&foo=bar

Access query parameters with $route.query

To access a query parameter, we use the $route instance object with the query property and the name of the property we want to access.

Syntax: $route.query
$route.query.propertyName

As an example, let’s pull the user’s name from the query parameter and display it on the UserSingle page.

tip We can do it directly in the template, or store it in something like a data or computed property first.

Example: src/views/UserSingle.vue
<template>
  <h2>Single User page</h2>
  <p>({{ id }}) {{ name }}</p>
</template>

<script>
export default {
  props: ['id'],
  computed: {
    name() {
      return this.$route.query.name
    }
  }
}
</script>

If we run the example in the browser and click on one of the users, it will show the user’s name alongside their id.

Nested routes

The Vue router allows us to nest child routes in our main routes. To do this we add a children option to a route that takes an array of child route objects.

For example, let’s say we don’t want UserSingle to be its own page anymore. We want to load it inside the Users view and display the user details below the list of users.

For that, all we need to do is create a children array in the Users route and put the UserSingle route inside that array.

Example: src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      name: 'Home',
      component: () => import('../views/Home')
    },
    {
      path: '/user',
      name: 'Users',
      component: () => import('../views/Users'),
      children: [
        {
          path: '/user/:id',
          name: 'UserSingle',
          component: () => import('../views/UserSingle'),
          props: true
        }
      ]
    }
  ]
})

export default router

But if we load it up in the browser, it won’t work. That’s because Vue doesn’t know where we want to display the child route yet, so we need to add a <router-view> where we want the child routes to be rendered.

In our example, we’ll add it to the Users view.

Example: src/views/Users.vue
<template>
  <h2>Users page</h2>

  <p v-for="user in users" :key="user.id">
    <router-link
      :to="{
        name: 'UserSingle',
        params: { id: user.id },
        query: { name: user.name }
      }">
      ({{ user.id }}) {{ user.name }}
    </router-link>
  </p><hr>

  <!-- Show this component's child routes -->
  <router-view/>
</template>

<script>
export default {
  data() {
    return {
      users: [
        { id: 1, name: 'John' },
        { id: 2, name: 'Jane' },
        { id: 3, name: 'Jack' },
        { id: 4, name: 'Jill' }
      ]
    }
  }
}
</script>

If we run the example in the browser and click on a user, it will show that user’s details below the list.

Routes can be nested multiple levels deep, depending on your needs.

Example: deep nesting
{
  path: '/parent-path',
  name: 'ParentName',
  component: ParentView,
  children: [
    {
      path: '/child-path',
      name: 'ChildName',
      component: ChildView,
      children: [
        {
          path: '/child-of-child-path',
          name: 'ChildOfChildName',
          component: ChildOfChildView,
          children: [
            { ...etc }
          ]
        }
      ]
    }
  ]
}

Named router views

Vue allows us to have multiple router-view s in one component, provided we name them.

Syntax: named router views
<router-view name="header" />
<router-view name="footer" />
// etc.

We can then set up the components that are loaded for each router-view with the components option.

This option takes an object with key:value pairs where the key is the router-view ’s name, and the value is the component to load for that router-view .

Syntax: components option
{
  path: '/url-path',
  name: 'ViewName',
  components: {
    main:   ComponentForHeader,
    footer: ComponentForFooter,
  }
}

Like with slots , we can have one unnamed router-view . If we do, we specify its key as default .

Example: default router view
{
  path: '/url-path',
  name: 'ViewName',
  components: {
    main:   ComponentForHeader,
    footer: ComponentForFooter,
    default:ComponentForUnnamed
  }
}

Let’s see an example.

By default, the router-view in the root App component shows the Home view. Let’s add a second router-view with the name “users”.

Example: src/App.vue
<template>
  <div>
    <router-link :to="{ name: 'Home'  }">Home</router-link> |
    <router-link :to="{ name: 'Users' }">Users</router-link>
  </div>

  <router-view/>
  <router-view name="users" />
</template>

Then we’ll modify the Home route to load both the Home and Users views in their respective router-view s.

Example: src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'

import Home from '../views/Home'
import Users from '../views/Users'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      name: 'Home',
      components: {
        // load 'Users' component
        // in the named router-view
        users:   () => import('../views/Users'),
        // load 'Home' component
        // in the unnamed router-view
        default: () => import('../views/Home')
      }
    },
    {
      path: '/user',
      name: 'Users',
      component: () => import('../views/Users')
    }
  ]
})

export default router

If we run the example in the browser, it will load both components instead of just the Home page.

Scroll behavior

Because views are loaded dynamically, a page won’t scroll back to the top when we navigate to a new route. Vue gives us the scollBehavior CODE option to customize such behavior.

This option is a method that automatically gets 3 parameters.

  • to is the route we just navigated to.
  • from is the route we came from.
  • savedPosition is the absolute position of where we were on the previous view with the top and left properties.
Example: scrollBehavior
scrollBehavior(to, from, savedPosition) {}

The method returns an object with values for the top and left properties. If we want to scroll to the top, we return both as 0.

Example: back to top
scrollBehavior(to, from, savedPosition) {

  return { top: 0, left: 0 }
}

When we navigate to a new route and a view loads, savedPosition will return null . But if we click on the back button in the browser, it will return the absolute value of where we were on that page.

If we want to return the user to that spot, we can do a null check to see if the user has clicked on the back button and if so, return that saved position.

Example: top or previous scroll location
scrollBehavior(to, from, savedPosition) {
  if (savedPosition) {
    return savedPosition
  }

  return { top: 0, left: 0 }
}

Let’s see an example.

In the root App component, we’ll add a spacer div before the router-view so that we can demonstrate the behavior. We’ll also remove the second router-view from the previous section.

Example: src/App.vue
<template>
  <div>
    <router-link :to="{ name: 'Home'  }">Home</router-link> |
    <router-link :to="{ name: 'Users' }">Users</router-link>
  </div>

  <div class="spacer"></div>
  <router-view/>
</template>

<style scoped>
.spacer {margin-top: 2000px}
</style>

In the router config, we can add the scrollBehavior method as is from the example above. To more clearly see what’s going on, we add a console log of the savedPosition parameter.

Example: src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'

import Home from '../views/Home'
import Users from '../views/Users'
import UserSingle from '../views/UserSingle'

const router = createRouter({
  history: createWebHistory(),
  scrollBehavior(to, from, savedPosition) {
    console.log('SavedPosition', savedPosition)

    if (savedPosition) {
      return savedPosition
    }

    return { top: 0, left: 0 }
  },
  routes: [
    {
      path: '/',
      name: 'Home',
      component: () => import('../views/Home')
    },
    {
      path: '/user',
      name: 'Users',
      component: () => import('../views/Users'),
      children: [
        {
          path: '/user/:id',
          name: 'UserSingle',
          component: () => import('../views/UserSingle'),
          props: true
        }
      ]
    }
  ]
})

export default router

When we run the example in the browser, the console log will show 0 for both top and left properties. If we navigate to Users, it will show null .

Then if we scroll down and click on a user, it will jump back to the top of the page like we want. The console log will show null again because we navigated to a new route.

Then if we click on the browser’s back button, it will take us to the bottom of the previous page where we clicked the user’s link. This time, the console log will show a positive value for the top property, something like {left: 0, top: 1485} .

Further Reading

For more information on the topics covered in this lesson, please see the relevant sections below.