Search code examples
vue.jsvue-componentv-forvue-reactivity

Why is Vue updating the all components instead of just the changed component in a v-for?


I've got a v-for list which contains a component. Inside the component I display either a <div> or a <input> depending on the value of a property. The problem is that when I change the property value, all of the items in the v-for are updated and not just the component that was changed. This is not an issue with small applications but I've noticed a significant perfomance decrease when working with larger datasets.

So basic question is:

How can I avoid all components being rendered when only one component is updated?

I put it all in a JSFiddle here. Notice that when you click the button to show C-component input that all components are rerendered (shown in console), and not just the C-component.

HTML

<div id="app">
<button @click="showinput = 'C'">
Show C input
</button>
<br>
<br>
<div v-for="item in list" :key="item.id">
  <list-item :item=item :showinput="showinput"></list-item>
  </div>
</div>

<template id="list-item">  <span>  <div v-if="showinput !== item.name">
{{item.name}}</div>
<input v-else
    type="text"
    v-model.lazy="item.name"  >
</span>
</template>

JS

Vue.component('list-item', {
  template: '#list-item',
  props: ['item', 'showinput'],
  data () {
  return   {

  }},
  beforeUpdate() {
    console.log("Updating " + this.item.name)
  }
});

// create a new Vue instance and mount it to our div element above with the id of app
var vm = new Vue({
  el: '#app',
  data: {
  list: [{name: "A", id: 1}, {name: "B", id: 2}, {name: "C", id: 3},],
      showinput: "X"
  }
});

Solution

  • All your components are using the same variable, showinput, and as such all of them are updated. It doesn’t matter that you end up with only one displayed, vue doesn’t know that. Actually you’re still rendering the spans, but they are just empty.

    What you should do is to filter the data and use that filtered array in your v-for.

    computed: {
      filteredList: function () {
         return this.list.filter( item => item.name === this.showInput )
       }
     }
    

    And then in your v-for

    <div v-for=“item in filteredList” ...
    

    LE

    If you want to show all the information, but change the presentation you can split list-item component in 2 components, one for the label and one for the input. Then use :is to choose what template to use (or even a v-if). I created this fiddle to see it in action. Note that onBeforeUpdate won't be called anymore, because vue recreates the component.

    It seems that in vue 3 this could be fixed, as it will support partial rendering of a component.