How to prevent component from collecting unneccessary dependencies?

Viewed 69

I have a component that is based on some nested state. E.g.

<rect width="some.deep.state.width">

Whenever any object in this path is altered (not newly set), the component re-renders even though nothing changed.

For example, the following leads to a rerender:

Vue.set(some.deep, 'newProp', something)

One can imagine, that this is highly undesired especially when using nested state in a store and some array values change.

In my case I, save multiple entities in my store and whenever a new instance of that entity is added, all components based on that entity rerender even though nothing changed.

So is there a way to prevent collecting this (technical) deps (pun intended)? Can I prevent Vue from collecting deps by some mechanism?

PS: I know, that this mechanism is useful in other places. In my case, it is not desired

Codesandbox which shows that behavior (in the console): https://codesandbox.io/s/vuejs-playground-forked-sx534?file=/components/hello.js

Inline Example - update is triggered:

const app = new Vue({
  el: '#app',
  data: function () {
    return { some: { nested: { prop: 6 }}}
  },
  created: function () {
    console.log('component created')
    
    setTimeout(function () {
    
      Vue.set(this.some, 'otherProp', 10)
    
    }.bind(this), 2000)
  },
  updated: function () {
    console.log('component updated')
  }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
 Prop: {{ some.nested.prop }}
</div>

2 Answers

I think this problem isn't prevent component from collecting unneccessary dependencies. Vue renderWatcher collected some.deep dependencies already when you using <rect width="some.deep.state.width"> in template. Then executing Vue.set(this.some.deep, 'newProp', something) will notify all collected some.deep watcher which includes renderWatcher which will trigger rerender.

I have a hack solution which are stop renderWatcher running if you ensure that you wan't to rerender.

const app = new Vue({
  el: '#app',
  data: function () {
    return { some: { deep: { prop: 6 }}}
  },
  created: function () {
    console.log('component created')
    
    setTimeout(function () {
      console.log('trigger rerender?')
      Vue.set(this.some, 'otherProp', 10)
      this.preventRerender()
    }.bind(this), 2000)
  },
  render(h) {
    console.log('render')
    return h('div', this.some.deep.prop)
  },
  methods: {
    // keep calling at the code end
    preventRerender() {
      this._watcher.active = false
      this.$nextTick(() => {
        this._watcher.active = true
      })
    }
  }
})

You must notice that the updated hook is still triggered.

The way I ended up doing it was by watching the deeply nested properties. Watchers seem to not register for changes I explained in my question. Therefore I added a watcher for every property which sets a property on the component which in turn I can use in the template.

This solved the problem of unnecessary re-renders, however, it turned out that multiple watchers whose values change simultaneously each trigger a rerender. This is strange since all watchers execute in one dom cycle and therefore should only trigger one rerender. If anyone knows the reason for this, feel free to elaborate!

const app = new Vue({
  el: '#app',
  data: function () {
    return {
      some: { nested: { prop: 6 }},
      theProp: null
    }
  },
  created: function () {
    console.log('component created')
    
    this.$watch(() => this.some.nested.prop, (newValue) => this.theProp = newValue, {immediate: true})
    
    setTimeout(() => {
    
      // Does not triggers update
      Vue.set(this.some, 'otherProp', 10)
    
    }, 2000)
    
    setTimeout(() => {
    
      // Triggers update
      this.some.nested.prop = 10
    
    }, 4000)
  },
  updated: function () {
    console.log('component updated')
  }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
 Prop: {{ theProp }}
</div>

Related