简体   繁体   中英

Vue.js computed property loses its reactivity when passed through an event

I have a Modal component in my main app that gets passed content via an event whenever a modal has to be shown. Modal content is always a list with an action associated with each item, like "select" or "remove":

Vue.component('modal', {
  data() {
    return {
      shown: false,
      items: [],
      callback: ()=>{}
    }
  },
  mounted() {
    EventBus.$on('showModal', this.show);
  },
  template: `<ul v-if="shown">
    <li v-for="item in items">
      {{ item }} <button @click="callback(item)">Remove</button>
    </li>
  </ul>`,
  methods: {
    show(items, callback) {
      this.shown = true;
      this.items = items;
      this.callback = callback;
    }
  }
});

Sadly, when passing a computed property to that modal like in the component below, the reactive link gets broken -> if the action is "remove", the list is not updated.

Vue.component('comp', {
  data() {
    return {obj: {a: 'foo', b: 'bar'}}
  },
  computed: {
    objKeys() {
      return Object.keys(this.obj);
    }
  },
  template: `<div>
    <button @click="showModal">Show Modal</button>
    <modal></modal>
  </div>`,
  methods: {
    remove(name) {
      this.$delete(this.obj, name);
    },
    showModal() {
      EventBus.$emit('showModal', this.objKeys, this.remove);
    }
  }
});

See the minimal use case in this fiddle: https://jsfiddle.net/christophfriedrich/cm778wgj/14/

I think this is a bug - shouldn't Vue remember that objKeys is used for rendering in Modal and update it? (The forwarding of the change of obj to objKeys works.) If not, what am I getting wrong and how could I achieve my desired result?

You are passing a value to a function, you are not passing a prop to a component. Props are reactive, but values are just values. You include modal in the template of comp , so rework it to take (at least) items as a prop. Then it will be reactive.

I would recommend having the remove process follow the emit-event-and-process-in-parent rather than passing a callback.

 const EventBus = new Vue(); Vue.component('comp', { data() { return { obj: { a: 'foo', b: 'bar' } } }, computed: { objKeys() { return Object.keys(this.obj); } }, template: `<div> <div>Entire object: {{ obj }}</div> <div>Just the keys: {{ objKeys }}</div> <button @click="remove('a')">Remove a</button> <button @click="remove('b')">Remove b</button> <button @click="showModal">Show Modal</button> <modal :items="objKeys" event-name="remove" @remove="remove"></modal> </div>`, methods: { remove(name) { this.$delete(this.obj, name); }, showModal() { EventBus.$emit('showModal'); } } }); Vue.component('modal', { props: ['items', 'eventName'], data() { return { shown: false, } }, mounted() { EventBus.$on('showModal', this.show); }, template: `<div v-if="shown"> <ul v-if="items.length>0"> <li v-for="item in items"> {{ item }} <button @click="emitEvent(item)">Remove</button> </li> </ul> <em v-else>empty</em> </div>`, methods: { show(items, callback) { this.shown = true; }, emitEvent(item) { this.$emit(this.eventName, item); } } }); var app = new Vue({ el: '#app' }) 
 <script src="//unpkg.com/vue@latest/dist/vue.js"></script> <div id="app"> <comp></comp> </div> 

You have the modal working with its own copy of items :

 template: `<ul v-if="shown">
    <li v-for="item in items">
      {{ item }} <button @click="callback(item)">Remove</button>
    </li>
  </ul>`,
  methods: {
    show(items, callback) {
      this.shown = true;
      this.items = items;
      this.callback = callback;
    }
  }

That copy is made once, upon the call to show , and what you are copying is just the value of the computed at the time you emit the showModal event. What show receives is not a computed, and what it assigns is not a computed. It's just a value.

If, anywhere in your code, you made an assignment like

someDataItem = someComputed;

the data item would not be a functional copy of the computed, it would be a snapshot of its value at the time of the assignment. This is why copying values around in Vue is a bad practice: they don't automatically stay in sync.

Instead of copying values around, you can pass a function that returns the value of interest; effectively a get function. For syntactic clarity, you can make a computed based on that function. Then your code becomes

 const EventBus = new Vue(); Vue.component('comp', { data() { return { obj: { a: 'foo', b: 'bar' } } }, computed: { objKeys() { return Object.keys(this.obj); } }, template: `<div> <div>Entire object: {{ obj }}</div> <div>Just the keys: {{ objKeys }}</div> <button @click="remove('a')">Remove a</button> <button @click="remove('b')">Remove b</button> <button @click="showModal">Show Modal</button> <modal></modal> </div>`, methods: { remove(name) { this.$delete(this.obj, name); }, showModal() { EventBus.$emit('showModal', () => this.objKeys, this.remove); } } }); Vue.component('modal', { data() { return { shown: false, getItems: null, callback: () => {} } }, mounted() { EventBus.$on('showModal', this.show); }, template: `<div v-if="shown"> <ul v-if="items.length>0"> <li v-for="item in items"> {{ item }} <button @click="callback(item)">Remove</button> </li> </ul> <em v-else>empty</em> </div>`, computed: { items() { return this.getItems && this.getItems(); } }, methods: { show(getItems, callback) { this.shown = true; this.getItems = getItems; this.callback = callback; } } }); var app = new Vue({ el: '#app' }) 
 <script src="//unpkg.com/vue@latest/dist/vue.js"></script> <div id="app"> <comp></comp> </div> 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM