Maya
Maya

Reputation: 1412

How to prevent parent component from reloading when changing a parameterised child component in Vue js

I have a page where a ClientPortfolio (parent component) containing a list of Securities (child component) are loaded in a v-data-table list.

enter image description here

The issue I have is that ClientPortfolio is fully reloaded every time I click on a security in the list causing the entire list to be refreshed causing scroll and selected class to reset, as well as unncessary performance overhead. I have looked at the documentation of Vue and nothing seems to point out how to only refresh a child component when it has parameters, it looks like the parent component is being refreshed as the route is changing every time a security is selected, despite expecting that Vue would know that only sub (nested route) is changing hence need to only reload the child component

enter image description here

The closest answer I got was explained on https://github.com/vuejs/vue-router/issues/230 which does not explain in the code how to achieve this.

routes.js:

routes: [
    {
      path: '/client/:clientno/portfolios/:portfolioNo',
      component: ClientPortfolios,
      children: [
        { path: 'security/:securityNo', component: Security }
      ]     
    }, 
  ]

Router link in ClientPortfolios.vue:

 <router-link tag="tr" style="cursor:pointer"
              :to="`/client/${$route.params.clientno}/portfolios/${selectedPortfolioSequenceNo}/security/${props.item.SecurityNo}-${props.item.SequenceNo}`"
              :key="props.item.SecurityNo+props.item.SequenceNo">

            </router-link>

Router view (for Security component) in ClientPortfolios.vue:

<v-flex xs10 ml-2>
      <v-layout>
          <router-view :key="$route.fullPath"></router-view>
      </v-layout>
    </v-flex>

Any hint on how to prevent parent from getting reloaded is appreciated.

EDIT: Trying to get closer to the issue, I notice that the "Key" attr in ClientPortfolios changes (as shown in the Vue debug window above) whenever I change the Security, could that be the reason? Is there a way to assign a key to ClientPortfolios component although its not a child one? Or a way to not update its key when navigating to different securities?

UPDATE: Full code

ClientPortfolios.vue

<template>
  <v-layout row fill-height>
    <v-flex xs2>
      <v-layout column class="ma-0 pa-0 elevation-1">
        <v-flex>
          <v-select v-model="selectedPortfolioSequenceNo" :items="clientPortfolios" box label="Portfolio"
            item-text="SequenceNo" item-value="SequenceNo" v-on:change="changePortfolio">
          </v-select>
        </v-flex>
        <v-data-table disable-initial-sort :items="securities" item-key="Id" hide-headers hide-actions
          style="overflow-y: auto;display:block;height: calc(100vh - 135px);">
          <template slot="items" slot-scope="props">
            <router-link tag="tr" style="cursor:pointer"
              :to="{ name: 'Security', params: { securityNo: props.item.SecurityNo+'-'+props.item.SequenceNo } }"
              >
            </router-link>

          </template>
          <template v-slot:no-data>
            <v-flex class="text-xs-center">
              No securities found
            </v-flex>
          </template>
        </v-data-table>
      </v-layout>
    </v-flex>

    <v-flex xs10 ml-2>
      <v-layout>
        <keep-alive>
          <router-view></router-view>
        </keep-alive>
      </v-layout>
    </v-flex>
  </v-layout>

</template>
<script>
  import Security from '@/components/Security'

  export default {
    components: {

      security: Security
    },
    data () {
      return {
        portfoliosLoading: false,
        selectedPortfolioSequenceNo: this.$route.params.portfolioNo,
        selectedPortfolio: null,
        securityNo: this.$route.params.securityNo
      }
    },
    computed: {
      clientPortfolios () {
        return this.$store.state.ClientPortfolios
      },
      securities () {
        if (this.clientPortfolios == null || this.clientPortfolios.length < 1) {
          return []
        }
        let self = this
        this.selectedPortfolio = global.jQuery.grep(this.clientPortfolios, function (portfolio, i) {
          return portfolio.SequenceNo === self.selectedPortfolioSequenceNo
        })[0]

        return this.selectedPortfolio.Securities
      }
    },
    mounted () {
      this.getClientPortfolios()
    },
    activated () {
    },
    methods: {
      changePortfolio () {
        this.$router.push({
          path: '/client/' + this.$route.params.clientno + '/portfolios/' + this.selectedPortfolioSequenceNo
        })
      },
      getClientPortfolios: function () {
        this.portfoliosLoading = true
        let self = this
        this.$store.dispatch('getClientPortfolios', {
          clientNo: this.$route.params.clientno
        }).then(function (serverResponse) {
          self.portfoliosLoading = false
        })
      }
    }
  }
</script>

Security.vue

<template>
  <v-flex>
    <v-layout class="screen-header">
      <v-flex class="screen-title">Security Details </v-flex>

    </v-layout>
    <v-divider></v-divider>
    <v-layout align-center justify-space-between row class="contents-placeholder" mb-3 pa-2>
      <v-layout column>
        <v-flex class="form-group" id="security-portfolio-selector">
          <label class="screen-label">Sequence</label>
          <span class="screen-value">{{security.SequenceNo}}</span>
        </v-flex>
        <v-flex class="form-group">
          <label class="screen-label">Security</label>
          <span class="screen-value">{{security.SecurityNo}}-{{security.SequenceNo}}</span>
        </v-flex>

        <v-flex class="form-group">
          <label class="screen-label">Status</label>
          <span class="screen-value-code" v-if="security.Status !== ''">{{security.Status}}</span>
        </v-flex>
      </v-layout>

    </v-layout>

  </v-flex>

</template>
<script>
  export default {
    props: ['securityNo'],
    data () {
      return {
        clientNo: this.$route.params.clientno,
        securityDetailsLoading: false
      }
    },
    computed: {
      security () {
        return this.$store.state.SecurityDetails
      }
    },

    created () {
      if (this.securityNo.length > 1) {
        this.getSecurityDetails()
      }
    },

    methods: {
      getSecurityDetails: function () {
        let self = this
        this.securityDetailsLoading = true

        this.$store.dispatch('getSecurityDetails', {
          securityNo: this.securityNo,
          clientNo: this.clientNo

        }).then(function (serverResponse) {
          self.securityDetailsLoading = false
        })
      }
    }
  }
</script>

router.js

const router = new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      component: Dashboard
    },
    {
      path: '/client/:clientno/details',
      component: Client,
      props: true
    },

    {
      path: '/client/:clientno/portfolios/:portfolioNo',
      component: ClientPortfolios,
      name: 'ClientPortfolios',
      children: [
        { path: 'security/:securityNo',
          component: Security,
          name: 'Security'
        }
      ]
    }
  ]
})

UPDATE:

Just to update this as it’s been a while, I finally got to find out what the problem is, which is what @matpie indicated elsewhere, I have found out that my App.vue is the culprit where there is a :key add to the very root of the application: <router-view :key="$route.fullPath" /> this was a template I used from somewhere but never had to look at as it was "working", after removing the key, all is working as it should, marking matpie answer accepted.

Upvotes: 23

Views: 16157

Answers (4)

fullmetal
fullmetal

Reputation: 414

Could you please check again after removing the local registration of the security component? As it's not needed because this is being handled by the vue router itself.

components: { // delete this code

      security: Security
    },

Upvotes: -1

matpie
matpie

Reputation: 17512

Preventing component reload is the default behavior in Vue.js. Vue's reactivity system automatically maps property dependencies and only performs the minimal amount of work to ensure the DOM is current.

By using a :key attribute anywhere, you are telling Vue.js that this element or component should only match when the keys match. If the keys don't match, the old one will be destroyed and a new one created.

It looks like you're also pulling in route parameters on the data object (Security.vue). Those will not update when the route parameters change, you should pull them in to a computed property so that they will always stay up-to-date.

export default {
  computed: {
    clientNo: (vm) => vm.$route.params.clientno,
  }
}

That will ensure that clientNo always matches what is found in the router, regardless of whether Vue decides to re-use this component instance. If you need to perform other side-effects when clientNo changes, you can add a watcher:

vm.$watch("clientNo", (clientNo) => { /* ... */ })

Upvotes: 16

Ivan Klochkov
Ivan Klochkov

Reputation: 702

I had a similar issue once. IMO it was caused by path string parsing. Try to set a name for your route. And replace your router-link to param with an object. And remove router-view :key prop. It doesn't need to be there. It is used to force component update when a route changes. It is usually a sign of bad code. Your component (Security) should react to route params update. Not the parent component force it to.

So, try to change your code to:

routes: [
    {
      path: '/client/:clientno/portfolios/:portfolioNo',
      component: ClientPortfolios,
      name: "ClientPortfoliosName", // it can be anything you want. It`s just an alias for internal use.
      children: [
        { 
           path: 'security/:securityNo', 
           name: "PortfolioSecurities", // anyway, consider setting route names as good practice
           component: Security 
        }
      ]     
    }, 
  ]
 <router-link tag="tr" style="cursor:pointer"
              :to="{ name: 'PortfolioSecurities', params: { clientno: $route.params.clientno, portfolioNo: selectedPortfolioSequenceNo, securityNo: props.item.SecurityNo+'-'+props.item.SequenceNo } }"
              :key="props.item.SecurityNo+props.item.SequenceNo">

            </router-link>

And it should work.

P.S. In your router-link you shall point to the route you want to navigate to. In this case PortfolioSecurities

Upvotes: -3

Kajal
Kajal

Reputation: 739

Instead of using router here. Declare two variable at root level for selected security and portfolio,

list the securities based on the selected portfolio.

on selecting a security from displayed securities, update the root variable using,

this.$root.selectedSecurityId = id;

you can have watch at security component level.

In root,

<security selectedid="selectedSecurityId" />

In component security,

....
watch:{
   selectedid:function(){
   //fetch info and show
   }
}
... 

the components will be look like following,

<portfolio>
  //active. list goes here
</portfolio>
........
<security selectedid="selectedSecurityId">
//info goes here
</security>

Above approach will help to avoid routers. hope this will help.

Upvotes: -2

Related Questions