loading...
Cover image for Vue Router Architecture and Nested Routes

Vue Router Architecture and Nested Routes

berniwittmann profile image Bernhard Wittmann 惻Updated on 惻3 min read

Working with nested routes can be quite a hassle...

Therefore I did come up with a conclusion on how to keep the routing configuration clean and to ease the working with nested routes.

There are already some tutorials on how to work with Vue Router in general, but I want to focus on this specific issue and the architecture around it.

I'll guide you through the creation of the example repo below and show you how I solved this

The main issue

The main problem is that if you want to nest routes the parent route component needs to always contain a <router-view></router-view>. This can be quite annoying sometimes and in my opinion it just does not feel right. Also this leads to high coupling between the component itself and its sub-routes / the routing.

Step 1: Add an EmptyRouterView Component

At first we create an EmptyRouterView Component, which just contains <router-view></router-view>.

<template>
  <router-view></router-view>
</template>

That is nearly the whole magic. Now we just need to use this component correctly in our route configuration

Step 2: Adjust your Components to the new architecture

Head over to your route config (probably router.js)

The current config will probably look like this

// Old Config
  {
      path: '/my-route',
      component: MyRouteView,
      name: 'route',
      children: [...]
  }

Let's change it like this:

// New Config
 {
      path: '/my-route',
      component: EmptyRouterView,
      children: [{
        name: 'route',
        path: '',
        component: MyRouteView
      }]
  }

We declare some kind of abstract Route with the EmptyRouterView as component. as a default child component we now add our old route configuration, but with an empty path. The empty path leads to this child route to be rendered when a user navigates to /my-route.

You'll also notice, that we gave our abstract parent route no name. This also helps, since when we add links to our application and refer to routes by name, we won't accidentally render the empty parent route.

Step 3: Nesting Routes

Now we can easily nest routes:

// Nested Config
 {
      path: '/my-route',
      component: EmptyRouterView,
      children: [{
        name: 'route',
        path: '',
        component: MyRouteView
      }, {
        name: 'route.child-one'
        path: 'child-one',
        component: ChildOneView
      }, {
        name: 'route.child-two'
        path: 'child-two',
        component: ChildTwoView
      }]
  }

See how easy it is to nest routes šŸ’ŖšŸ» When a user navigates to /my-route/child-one the ChildOneView should be rendered as intended.

Step 4: Deep Nesting Routes

We can also nest routes in our nested routes. Some kind of Route-ception

(Yes I know: bad wordplay šŸ˜)

// Deep Nested Config
 {
      path: '/my-route',
      component: EmptyRouterView,
      children: [{
        name: 'route',
        path: '',
        component: MyRouteView
      }, {
        path: 'sub',
        component: EmptyRouterView,
        children: [{
            name: 'deep',
            path: '',
            component: MyDeepRouteView
          }, {
            name: 'deep.child-one'
            path: 'deep-one',
            component: ChildOneView
          }, {
            name: 'route.child-two'
            path: 'deep-two',
            component: ChildTwoView
          }]
      }]
  }

The path /my-route/sub/deep-one would lead to ChildOneView beeing rendered.

I think I reached my word limit of the word routes, therefore I'll leave you with this. I condensed all of this in a repo, where you can see it all in action

GitHub logo BerniWittmann / vue-router-architecture

My Architecture approach on Vue Router

My Vue Router Architecture Approach

This repository should demonstrate my basic approach on Vue Router Architecture and nested routes. It also displays the possibility of handling dialogs with Vue Router

I use a view called EmptyRouterView (which just contains a router view, and one for the dialog) to achieve a clean way to structure my nested routing configuration and an easy way to handle dialogs.

The corresponding Blog Post can be found on dev.to

Part 1: Route Handling

Part 2: Dialog Handling

Project setup

npm install

Compiles and hot-reloads for development

npm run serve

ā“ Got any questions?

Don't hesitate to drop me an email to dev@bernhardwittmann.com

Short Spoiler: Modals

In the next part of this little series on Vue Router Architecture I would like to talk about handling Modals with Vue Router. The approach I explained above also has another advantage. It makes it very easy to deal with Dialogs (These Popups/Modals, I never know what to call them šŸ™„). Find Part 2 here:

I know there may be a hundred different approaches on this topic and I'd be glad to hear your thoughts on this šŸ˜‰ And as always: should you have any questions, don't hesitate to ask

Peace Out

Discussion

pic
Editor guide
Collapse
olen_d profile image
Olen Daelhousen

Thanks, this was super helpful. I was setting up an onboarding flow with the top level route being the initial account creation form, which then led to a profile form, etc. and trying to get the initial form to go away when the child routes were hit was driving me nuts. The absract EmptyRouterView worked like a charm.

Collapse
campgurus profile image
campgurus

Trying to understand how this is different than having all the "parent" and child routes at the top level in the first place. Seems like this has parent and child on the same level, right?

Collapse
berniwittmann profile image
Bernhard Wittmann Author

Iā€™m not sure whether I understand you correctly.
You ask where the difference is between having these nested routes and the approach to not having nested routes and having all of them at the top level of the configuration?
If so, nested routes allow you to reuse code and logic. Imagine a detail view of a product in a store. You have a route to view the single product and edit it. With nested route you can reuse the code required for fetching the product or adding an access control.

Collapse
campgurus profile image
campgurus

In the original vue app, I presume that you have a file App.vue, which essentially serves as the EmptyRouterView for all of your other views, right? Without nesting routes, you could have separate routes like products/:id/details and products/:id/edit, etc. The solution described in the article seems to recreate this structure. If the parent is truly empty (my App.vue actually contains my navigation and notification components) and the others are at the same level, I don't see how you can reuse code any more than I reuse code from App.vue. What am I missing? For example, I am creating a structure that has Projects and Documents s.t. :doc belongs_to :project and :project has_many :docs and nesting sees to make logical sense there. I just want to do it the right way.

Thread Thread
berniwittmann profile image
Bernhard Wittmann Author

Okay first of all let me summarize the two approaches:

First Approach: No Nested Routes

Main App.vue includes the application layout with nav, footer etc.

<template>
  <nav/>
  <router-view/>
  <footer/>
</template>

The router config has the routes on the top - level

[{
  name: 'view-product'
  path: '/product/:id/'
  beforeEnter: () => {
    checkAccess();
    loadProduct();
  }
}, {
  name: 'edit_product',
  path: '/product/:id/edit',
  beforeEnter: () => {
    checkAccess();
    loadProduct();
  }
}]

The pages themselves then mainly just contain the content of the page

<template>
  <h1>My Product</h1>
  <div>...</div>
</template>

Benefits: Application layout is reused, simple
Shortcomings: nearly no code-reuse regarding routing is possible (e.g. navigation guards, path definitions)

Second Approach: Nested Routes

The App.vue file is just the root router view

<template>
  <router-view/>
</template>

The router config is nested

[{
  path: '/product/:id/'
  beforeEnter: () => {
    checkAccess();
    loadProduct();
  },
  children: [{
    name: 'view_product',
    path: ''
  }, {
    name: 'edit_product',
    path: 'edit'
}]

The pages then contain the whole application code:

<template>
  <nav/>
  <h1>My Product</h1>
  <div>...</div>
  <footer/>
</template>

Benefits: code reuse in navigation, route structure resembles logical structure of entities
Shortcomings: Pages include duplicate code regarding application structure

My preferred approach: Nested routes with layouts

Now we can use the concept of layouts, to enable the pages to reuse application structure. This way we can diminish the shortcomings of the bare nested router approach. But in general the layouts are independent from the routing approach and just a good practice imo.
Layouts are simple components that hold the application structure (nav, footer, side-menu) for different pages.

<template>
  <nav/>
  <div id="content">
    <slot/> <!-- this is where the content will be injected -->
  </div>
  <footer/>
</template>

Then the pages can use these layouts like so:

<template>
  <layout-component>
    <h1>My Product</h1>
    <div>...</div>
  </layout-component>
</template>

That way the pages do not repeat the basic code for application structure. Also you can easily have different layouts for different pages. For example, some pages maybe should have a sidebar, while others shouldn't. This can easily be abstracted from the pages.

If you want to know more about the concept of layouts, I can also write a more detailed post on this. :)

Thread Thread
campgurus profile image
campgurus

Ok this is helpful, thanks. I hadn't thought of the beforeEnter callbacks. I had been using mounted/created for that in each component, but if I understand it correctly, I like the abstraction. Now I am wondering why we can't just keep reusing the layout stuff in App.vue and do the nesting with your empty parent solution described in your article. I haven't used slots much so that is something I need to learn. So far props have sufficed for me.

Thread Thread
berniwittmann profile image
Bernhard Wittmann Author

I would keep the App.vue and the EmptyRouterViewComponent just empty with the <router-view/>. This way the pages can control the whole appearance (through the layouts). I would let the pages do this (and not the App.vue). Imagine you would hard-code the nav and a side menu into the App.vue and now you want a Login page not to have a side menu. This may be pretty difficult to override that way. When the pages control the layout, this is way more dynamic and adaptable.

You can also take a look at this project, which uses the approach I outline in the comment above, to understand how this would work on a more complex application than the example: github.com/BerniWittmann/cape-fron...

In general slots are a very handy tool, so i would recommand taking a more in-depth look (e.g. named slots). Especially components can profit from slots. For example, a card component that renders some content is more versatile with a slot instead of props for the content.

Thread Thread
campgurus profile image
campgurus

Ok. I'm converted. I am going to check out slots.

Thanks again.

Collapse
whiteman_james profile image
James Whiteman

I found I had to add a bit more, because you can't have name github.com/vuejs/vue-router/issues...
Things like breadcrumbs won't work hence I adding something like meta.label to your route enabled me to filter and add breadcrumbs

this.$route.matched.filter((route) => route.name || route.meta.label).map( (route) => {
   let name = route.name
   if(route.meta.label){
      name = route.meta.label
   }
   let items = {
     'text': name,
   }
   return items
})

Naming the label the same as the default child allow a breadcrumb in the place of the empty component

Collapse
berniwittmann profile image
Bernhard Wittmann Author

Good point. I think it's a good idea to use meta.label for a Breadcrumb navigation anyway, since the translation keys mostly have a different convention than route names and therefore probably are different. šŸ‘šŸ»