Why does changing nested routes reload the parent route?

If you’ve got a simple nested route like so:

{ path: '/user/:id', component: UserProfile,
      children: [
        {
          path: 'comments',
          component: UserComments
        },
        {
          path: 'posts',
          component: UserPosts
        }
      ]
    }

And as is common you’re doing an axios request to get the user within the mounted method in the UserProfile component.

Then if you switch between comments and posts it will constantly reload the UserProfile component as well, repeating the axios request.

Why doesn’t it cache the parent component and only load the nested route?

Is there a way to get that behaviour?

That’s not normal behaviour, please share code.

Ah i’ve figured out what was causing it. It’s because the top had a key.

<transition name="fade" mode="out-in">
                    <router-view :key="$route.path"></router-view>
                </transition>

But from my understanding I need this in order for the transitions to work? Otherwise moving between two pages that use the same component won’t cause a transition?

Is there another way around this?

Which router view is this? For the UserProfile or its child routes?

That’s for the UserProfile the main router-view

Then you could use a more specific key, e.g. Involving the $route.params.id.

I realize that not all main routes may have one though. In that case you should use a computed property to create a key that is unique to each main route, and use that computed prop as key.

Hmm i’m struggling to come up with a solution that’s generic enough. If you have a main then it needs to have a :key that works for any route which could have any format or number of params or children.

At first I tried this:

computed: {
    routeKey() {
        return JSON.stringify([this.$route.matched[0].name, this.$route.params]);
    }
}

This gets the first route UserProfile (I gave it a name, I didn’t in my original example) and the params. But then if you have something like this nested route underneath.

{
      path: 'posts/:type?',
      component: UserPosts
    }

Then the type will be included and the params and be encoded into the key making it unique and refresh the parent UserProfile component.

It would be nice if you could access this.$route.parent but that doesn’t seem to be available.

This feels like a really simple and generic problem but i’ve been googling it last night and this morning with no luck.

I’m starting to think I should avoid having a transition around my main and do animations somewhere else such as beforeEach on the router. But then when you look at the docs it doesn’t suggest doing that anywhere and only suggests doing it the way i’ve said done with the transition component.

Maybe there’s something else i’m completely misunderstanding.

Am I wrong in thinking I need a :key on the main router view? I have it because otherwise if are on the UserProfile page and click on another user it won’t refresh even though the url changes. I could also solve this with a “watch” $route.

Hmm actually the transition animation has nothing to do with it. I still need the :key there otherwise the page won’t reload.

I can solve this by this

beforeRouteUpdate(to, from, next) {
        if (this.id!= to.params.id) {
            this.fetchData();
        }
        next()
    },

and

mounted() {
        this.fetchData();
    },

essentially calling the mounted method within a beforeRouteUpdate.

So then that leaves me with being unable to do an animation because that requires the :key but at least I can stop the component refreshing when the nested route changes.

I still feel like i’m probably doing something really stupid though since this seems like such a generic problem that probably everyone has.

The watch solution works for refeshing the data in the component (asuming I correctly understood what you mean), but it’s still the same component, there’s no transition possible.

However you have to note that you only need a key for dynamic routes (those involving params), as only for those you want to force a new instance to be created for every dynamic param. Static routes are fine by without a key.

So you could add a helper function to dynamic route’s meta field like this:

{
  path: '/profile/:id',
  name: 'UserProfile',
  component: UserProfile,
  meta: {
    key: route => `UserProfile-${route.params.id}`
  }
}

This helper function could create key unique to this route, and another dynamic route could have its own helper.

key: route => `forums-${route.params.forumId}`

Then your computed prop in the component would look like this:

computed: {
    routeKey() {
        const helper = this.$route.matched[0].meta.key
        return helper ? helper(this.$route) : undefined
    }
}

And just do show how this could be abstracted better:

You could wrap the transition & router view in a functional component, so instead of this:

<transition name="fade" mode="out-in">
  <router-view :key="routeKey"></router-view>
</transition>

you would have this:

<transition-router-view :level="0" name="fade" mode="out-in" />

The level tells the component for which index of $route.matched it should generate a key

Implementation:

export default {
  funtional: true,
  props: ['level'],
  render: (h, { attrs, data, parent, props }) => {
    const key = createKey(parent, props.level)
    
    const routerView = h('router-view', { key })

    const transitionData = { ...data, props: attrs, attrs: undefined }

    return h('transition', transitionData, [routerView])
  }
}

functional createKey(parent, level = 0) {
  const route = parent.$route
  const helper = route.matched[level] && route.matched[level].meta.key
  return helper ? helper(route) : undefined
}

…and you could re-use this in multiple places without having to write a computed prop for the routeKey over and over

In your example route.matches[level] won’t include the params so you won’t be able to do forums-${route.params.forumId} you’d need to send the route and the params separately or just the current route.

I’ve just got it working like so (I’ve kept my specific use case of Stadiums, I used users before so it was more familiar to the docs).

{
        path: '/stadium/:stadiumId',
        component: require('./pages/StadiumPage.vue'),
        props: true,
        name: 'stadium',
        meta: {
            keyBuilder: (route) => `stadium-${route.params.stadiumId}`
        },
        children: [
            {
                path: 'history',
                component: require('./pages/StadiumHistoryPage.vue'),
                name: 'stadium-history',
                props: true
            },
            {
                path: 'comments',
                component: require('./pages/StadiumCommentsPage.vue'),
                name: 'stadium-comments',
                props: true
            }
        ]
    },

And then on $root I have the computed property

computed: {
    routeKey() {
        const keyBuilder = this.$route.matched[0].meta.keyBuilder;
        return keyBuilder ? keyBuilder(this.$route) : this.$route.path;
    }
}

and I only have one main (is it common to have more than one?)

<transition name="fade" mode="out-in">
    <router-view :key="routeKey"></router-view>
</transition>

And a simple version of my StadiumPage might look like:

<template>
<div>
    <h1>{{ stadium.name }}</h1>
    <router-link :to="{ name: 'stadium-history'}">
        History
    </router-link>
    <router-link :to="{ name: 'stadium-comments'}">
        Comments
    </router-link>
    <transition name="fade" mode="out-in">
        <router-view :stadium="stadium"></router-view>
    </transition>
</div>

It doesn’t seem to be necessary to have a :key on the nested route?

right, silly mistake - i didn’t test this code I just wrote it down from the top of my head. Correct code:

functional createKey(parent, level = 0) {
  const route = parent.$route
  const helper = route.matched[level] && route.matched[level].meta.key
  return helper ? helper(route) : undefined
}

corrected in my previous post(s)

You usually only have one main router-view, sure - but that could serve for many sibling main routes.

Right, because the child routes are static.