Nuxt.js: Reuse component if the new path resolves to the same route

Created on 19 Feb 2019  路  22Comments  路  Source: nuxt/nuxt.js

What problem does this feature solve?

I'm aware of the watchQuery parameter but it only works for the query parameters. Consider the following case:

/report and /report/10 resolves to the same page component and when the user is in the /report page and saves the report, we redirect the user to the /report/10 but prevent Nuxt to reload the page component as we have all the data and the state is updated.

I tried to use watchQuery but it didn't work so I had to try native history.pushState but it seems to cause trouble in navigation. (user triggered back/forward buttons).

What does the proposed changes look like?

If /report and /report/10 resolves to the same route, when I call $route.push, I should be able to reuse the component so the page reload should not be triggered when an optional flag is enabled.

This feature request is available on Nuxt community (#c8686)
feature-request

Most helpful comment

Makes sense. 馃憤

All 22 comments

@manniL Unfortunately, the previous issue doesn't provide any reliable solution for this feature.

The first problem with the key approach is that on the latest version of Nuxt, it calls data() so cause the state to be reset even though we have key attribute which is a static value.

Also setting the value of key automatically disable the navigation and every $route.push that resolves to the same component ends up being impotent. It would be much better to have an API similar to watchQuery, the purpose of key seems to be different than this use-case.

I would vote for the issue to be re-opened.

Makes sense. 馃憤

I realized that even though I redirect the user to another route, the page redirect doesn't work when key is defined.

Hi @buremba

In this particular scenario, you want to tell Nuxt to not load any asyncData from the matched page when you call this.$router.push('/report/10') right?

Hey @Atinux, correct but the issue seems to be more complex than the asyncData issue. Nuxt doesn't really load pages even though the path resolves to another route.

Hi @buremba
Can you look at my issue #5257, Is it related to this discussion?

@webspilka I don't think so. This one is a feature request.

Side note: this would also somewhat help with #4132 because the transition (presumably) wouldn't trigger.

@johnRivs that seems to be the case. I guess that this issue has many side-effects.

Any updates on this issue?

I'm also awaiting updates on this issue, as I'm suffering the consequences of #4132 - API content appears -> transition occurs -> API content re-appears. In my case I can't use beforeMount() as suggested in that issue.

An FYI, it's a hack, but I was able to work-around Nuxt data() refreshing my component data on route change by copying forward the data from the previous life-cycle as folows

export default {
  key: '_bar',
  data () {
    return {
       myVariable: this.myVariable || 'my default value'
    }
  }
}

We also use these tricks as a workaround but it caused other issues in our app. Is there anyone working on this issue? If not, the core maintainers let me know where to look, I can try to create a PR maybe?

Hi @buremba

You need to look into packages/vue-app/packages/vue-app/template/client.js
But his behavior is tricky since it bypasse middleware/validate. What about using the store to keep the last page data and avoid fetching again if the same data?

@Atinux yes, we again found a workaround but it's fragile and causes more issues as we upgrade Nuxt. :( I will look at it when I have time and try to send a PR, thanks!

Any proper workaround for this?

I have a route like this /items/:id. All the items are fetched from api using asyncData, and displayed on the page. A tiny list of items is displayed to the user and when user clicks an item (which is a nuxt-link) the url changes, I watch the param $route.params.id and scroll the content to show the item existing on the page. However, I noticed 2 things that nuxt does which I don't want it to do:

  1. Remount the page component
  2. Re-run asyncData hook

For the issue 1, setting key: to a static value was the exact solution. However, for the issue 2 nuxt has nothing to offer, so I had to patch the module using patch-package to add an option to ignore route param change detection. Here it is, if it would help someone.


@nuxt+vue-app+2.13.3.patch

diff --git a/node_modules/@nuxt/vue-app/template/client.js b/node_modules/@nuxt/vue-app/template/client.js
index 1d3ba1f..e416fef 100644
--- a/node_modules/@nuxt/vue-app/template/client.js
+++ b/node_modules/@nuxt/vue-app/template/client.js
@@ -150,9 +150,31 @@ function mapTransitions (toComponents, to, from) {
 }
 <% } %>
 async function loadAsyncComponents (to, from, next) {
+  const loadComponents = () => resolveRouteComponents(to, (Component, instance) => ({ Component, instance }))
+
+
   // Check if route changed (this._routeChanged), only if the page is not an error (for validate())
   this._routeChanged = Boolean(app.nuxt.err) || from.name !== to.name
   this._paramChanged = !this._routeChanged && from.path !== to.path
+  if (this._paramChanged) {
+    const Components = await loadComponents()
+    const changedParams = []
+
+    Object.keys({ ...from.params, ...to.params }).forEach((key) => {
+      if (from.params[key] != to.params[key]) {
+        changedParams.push(key)
+      }
+    })
+
+    this._paramChanged = !Components.some(({ Component, instance }) => {
+      const ignoreParams = Component.options.ignoreParams
+
+      if (ignoreParams && !changedParams.filter(x => !ignoreParams.includes(x)).length) {
+        return true
+      }
+    })
+  }
+
   this._queryChanged = !this._paramChanged && from.fullPath !== to.fullPath
   this._diffQuery = (this._queryChanged ? getQueryDiff(to.query, from.query) : [])

@@ -164,10 +186,8 @@ async function loadAsyncComponents (to, from, next) {

   try {
     if (this._queryChanged) {
-      const Components = await resolveRouteComponents(
-        to,
-        (Component, instance) => ({ Component, instance })
-      )
+      const Components = await loadComponents()
+
       // Add a marker on each component that it needs to refresh or not
       const startLoader = Components.some(({ Component, instance }) => {
         const watchQuery = Component.options.watchQuery



@nuxt+types+0.7.9.patch

diff --git a/node_modules/@nuxt/types/app/vue.d.ts b/node_modules/@nuxt/types/app/vue.d.ts
index a58897e..dd89a55 100644
--- a/node_modules/@nuxt/types/app/vue.d.ts
+++ b/node_modules/@nuxt/types/app/vue.d.ts
@@ -18,6 +18,7 @@ declare module 'vue/types/options' {
     layout?: string | ((ctx: Context) => string)
     loading?: boolean
     middleware?: Middleware | Middleware[]
+    ignoreParams?: string[]
     scrollToTop?: boolean
     transition?: string | Transition | ((to: Route, from: Route) => string)
     validate?(ctx: Context): Promise<boolean> | boolean

Create a mixing that fixes both issues:

export function ignoreParamsMixin(...params: string[]) {
  return Vue.extend({
    /// Returns a route key with `params` excluded
    key: (route) => {
      const pathParts = route.matched[0].path.split('/')
      const actualParts = route.path.split('/')

      const keyParts = actualParts.map((x, i) => {
        const part = pathParts[i]
        if (part?.startsWith(':')) {
          const param = part.replace(/(^:)|(\?$)/g, '')

          if (params.includes(param)) {
            return ''
          }
        }

        return x
      })

      return keyParts.filter(x => x).join('-')
    },

    /// Used in the above patch to make nuxt not call `asyncData` if the specified params changed.
    ignoreParams: params,
  })
}

Usage:

/// my page component for `items/:id` route
export default Vue.extend({
  layout: '...',

  /// Make nuxt not remount the page component and not call asyncData hook if `id` param is changed
  mixins: [ignoreParamsMixin('id')],

  watch: {
     '$route.params.id'() {
          /// scroll to the item
     },
  },

  async asyncData() {...
})

We are using the route params as filters in our search page. So it makes sense for us to treat a param change (e.g., a category slug) and a query change (e.g., filter by featured) exactly the same way. Our URLs look like:

https://www.example.com/jobs/<category_slug>/<place_slug>?contract_type=<contract_type>&featured=<featured>

Luckily, we found the solution in the watchParam option :tada: that can be found in a few lines of the client.js file pointed out by @Atinux.

If watchParam: false then the data() isn't called, keeping the component state through consecutive URL changes. In other words, it works like the watchQuery, though you need a watcher on the $route to track the parameter changes (as probably you're already doing).


This is how we are using it with nuxt-property-decorator (uses vue-class-component):

import { Component, Vue, Watch } from 'nuxt-property-decorator';

@Component({
  key: 'JobsPage',
  watchParam: false
})
export default class JobsPage extends Vue {
  /**
   * Called on the first load, route params change, or query params change
   * NOTE: If you need to render initial data on SSR, then remove the `immediate` and use `fetch()` for the first load.
   */
  @Watch('$route', { immediate: true })
  async routeChanged(to: Route, from: Route) {
    // Check the route still matches the JobsPage component
    if (this.$route.name == 'jobs') {
      // Update filters based on the URL, and perform a new search.
    }
  }
}

As it isn't in the types declaration, we had to extend the interface. This is the content of our types/vue.d.ts:

import Vue from 'vue';

declare module '*.vue' {
  export default Vue;
}

declare module 'vue/types/options' {
  interface ComponentOptions<V extends Vue> {
    watchParam?: boolean;
  }
}

@manniL is there any reason why this parameter isn't documented? Shouldn't it be included in the types declaration? Thanks!

@buremba does it work for your use case?

Here is the PR where this feature was introduced: https://github.com/nuxt/nuxt.js/pull/6244

@emarbo we had to switch query parameters in order to make it work but it looks like a legit solution!

@manniL is there any reason why this parameter isn't documented? Shouldn't it be included in the types declaration? Thanks!

Must've been missed out. PR to the docs welcome!

Closing here as #6244 covers the issue.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

danieloprado picture danieloprado  路  3Comments

gary149 picture gary149  路  3Comments

VincentLoy picture VincentLoy  路  3Comments

o-alexandrov picture o-alexandrov  路  3Comments

uptownhr picture uptownhr  路  3Comments