Search code examples
vue.jsvue-reactivity

Why template rendering trigger in Vue differs between array item properties and computed array item properties?


Following the suggestion on what's the recommended approach for mutating props when "The prop is passed in as a raw value that needs to be transformed", I tried to "wrap" the original values (array in my case) and extend with whatever extra properties needed in the computed prop. But - regardless whether it's passed as prop or the components own data - I've found that the properties of the items in the computed array does not behave the same.

Even if I use the Vue.set() as it should when adding/modifying array item properties, if it is done directly on the data/props, it works as expected, the template gets rendered. But when using the same on the computed array, the template does not update, despite the object's properties are indeed updated under the hood.

Find below a simple repro (only using two items to showcase it is an array, but example would be the same if there would be only one item in the array):

<html>
<div id="app">
  <div>
    based on yolodata:
    <div v-for="y in yolodata" :key="y.name + y.wtf">
      {{y.wtf}}
      <button @click="yoloclick(y)">yolobtn</button>
    </div>
    <br>
    based on yolocomputed:
    <div v-for="y in yolocomputed" :key="y.original.wtf + y.wtf">      
      {{y.wtf}}
      <!-- {{y.original.wtf}} if this is uncommented, it re-renders when original is updated, but when only referenced in the :key, it does not -->
      <button @click="yoloclick(y)">yolobtn</button>
    </div>
    <br>
    actual yolodata:
    <div id="actualdata"></div>
    actual yolocomputed:
    <div id="actualcomputed"></div>
    <br>
    console:
    <div id="console">{{console}}</div>
  </div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script>
  new Vue({
    el: '#app', //Tells Vue to render in HTML element with id "app"
    data: function(){
      return{
        yolodata: [{name: 'yolo1', wtf:'wtf1'},{name: 'yolo2', wtf: 'wtf2'}],
        console: ''
      }
    },
    computed:{
      yolocomputed(){   
        console.log('yolocomputed evaluated')
        //eslint-disable-next-line   
        this.console += 'yolocomputed evaluated\n'
        return this.yolodata.map(y => ({original: y, wtf: 'wtf'}))
      }
    },
    methods:{
      yoloclick(item){
        this.$set(item, 'wtf', item.wtf + 'X')
        document.getElementById('actualdata').innerHTML = JSON.stringify(this.yolodata)
        document.getElementById('actualcomputed').innerHTML = JSON.stringify(this.yolocomputed)
      },
    },
  });
</script>
</html>

The computed property itself is only evaluated once, which is correct, because it's only direct dependency is yolodata which is never modified, only properties of the items inside it. When modifying items directly in yolodata (any of the first two buttons), both the first rendering works okay, and in the 'actual' part you can see the objects serialized, including the computed property showing the new values (because original item is stored by reference, so makes sense).

But the second for loop rendering seems off, even having the value as :key in the for loop from the original item, it does not update. If directly displaying it and not just using as a key (se commented line), then it does, which for me is already something I don't quite get. I know this is one way to trick vue into forcing a re-render, but why it doesn't apply here when used in the :key?

And if modifying items with Vue.set() directly in the computed array, when serialized it is clear the object has changed, and I would expect the second for loop to be updated accordingly since y.wtf is referenced directly in the template, but it doesn't do anything. It only gets rendered/updated if I then again modify something in the original array -> vue sees that somehow a dependency of the computed property has changed (?) even though the array itself didn't - BUT DOES IT ONLY ONCE. Afterwards, even if I keep updating items in the original array, the rendering of the computed property doesn't do anything.

I did read through the relevant Vue docs, but haven't found any exact details that would explain. From what I understand, in both cases the properties of the items in the array are modified using Vue.set() so it should be reactive in both cases.

In short, what I'm trying to achieve is exactly what was suggested in the Vue docs, use data/props as raw defaults and "decorate" that with needed extras in a computed property, instead of mutating it directly (in the example for simplicity I used simple data instead of props, but that doesn't change anything). In this case when we are talking about an Array of items, not a single prop, am I missing something or is this simply not supported for a reason I don't yet get?


Solution

  • The difference is that computeds are shallowly reactive, this doesn't cause issues as long as they are correctly used. In this case yolocomputed is mutated in yoloclick, which is not normal usage.

    A computed can generally be implemented with a combination of reactive data and a watcher, this is what needs to be done here, with deep option depending on the expected behaviour:

    data() {
      return{
        yolodata: [...],
        yolodatacopy: null
      },
    },
    watch: {
      yolodata: {
        immediate: true,
        deep: true,
        handler(new) {
          this.yolodatacopy = new.map(...);
        }
        ...