SaschaM78
SaschaM78

Reputation: 4497

Vue component not immediately updating after associated data changes

I am still new to Vue and struggle with having a component update its contents as soon as connected data has been updated. The component will show the changed content as soon as it has been re-rendered though.

Component

Vue.component('report-summary', {
  props: ['translation', 'store'],
  watch: {
    'store.state.currentModel.Definition': {
      handler: function(change) {
        console.log('Change detected', change);
      },
      deep:true
    }
  },
  template: '<div>
    '<div class="alert alert-secondary" role="alert">' +
    '<h5 class="border-bottom border-dark"> {{ translation.currently_defined }}</h5>' +
    '<div>{{ store.state.currentModel.Definition.length }} {{translation.elements }}</div>' +
    '</div>' +
    '</div>'
});

store is passed in as a property in the HTML of the page:

<report-summary :translation="t" :store="store"></report-summary>

store itself is a Vuex.Store:

let store = new Vuex.Store({
  state: {
    t: undefined,
    currentModel: undefined
  },
  mutations: {
    storeNewModel(state) {
      let model = CloneFactory.sealedClone(
        window.Project.Model.ReportTemplateModel);
      model.Header = CloneFactory.sealedClone(
        window.Project.Model.ReportTemplateHeaderModel);
      state.currentModel = model;
    },
    storeNewModelDefinition(state, definition) {
     state.currentModel.Definition.push(definition);
    }
  }
});

The currentModel element gets initialized by invoking store.commit('storeNewModel'); before any data is stored in the model's Definition attribute.

The definition's content gets updated by using store.commit('storeNewModelDefinition') in a loop:

for (let c in this.$data.currentDefinition.charts) {
  store.commit('storeNewModelDefinition', c);
}

When store.commit('storeNewModelDefinition', c); is called the store updates as expected:

Vuex%20Store|325x190

But the component won't react to the changed data:

Not%20updated|553x110

Then when I navigate away (by changing the view which hides the embedded component) and again navigate to the view the content got updated:

updated|476x112

In the Console window I see that the watcher hasn't been triggered at any point in time:

console|681x133

What am I missing here? Thanks a lot for opening my eyes in advance.

Using an EventBus

Switching the watch to an event bus listener also didn't work as planned.

This is the initialization of the EventBus:

window.eventBus= new Vue(); 

I added this to my Component:

  created: function() {
    window.eventBus.$on('report-summary-update', function() {
      this.$nextTick(function(){ this.$forceUpdate(); });
    });
  }

And emit the event after adding or removing elements from the list:

window.eventBus.$emit('report-summary-update');

When setting a breakpoint in the Debugger watching the this.$forceUpdate(); I see that it's also being called but the UI still will show the old content without any changes. I am really lost right now.


P.S. I cross-posted this on the Vue forum but as the community is rather small hope to receive some feedback here.

Upvotes: 8

Views: 16429

Answers (3)

Bereket Belete
Bereket Belete

Reputation: 374

From vuejs documentation Reactivity in depth section

a property must be present in the data object in order for Vue to convert it and make it reactive

and this is one-off vue reactivity caveat. Vue cannot detect property addition or deletion.

in your state instead of currentModel: undefined add all properties you want to be reactive like

currentModel: { definitions:[] }

then any addition or removal of an element from definitions array would automatically become reactive.

And you can find more explanation on the documentation

Reactivity in depth

Change detection caveats

Upvotes: 0

Ankit Kante
Ankit Kante

Reputation: 2139

Pushing data to a nested array always causes such issues where the component does not update.

Try adding a getter to your store and use that getter to get data from the store

let store = new Vuex.Store({
  state: {
    t: undefined,
    currentModel: undefined
  },
  mutations: {
    storeNewModel(state) {
      let model = CloneFactory.sealedClone(
        window.Project.Model.ReportTemplateModel);
      model.Header = CloneFactory.sealedClone(
        window.Project.Model.ReportTemplateHeaderModel);
      state.currentModel = model;
    },
    storeNewModelDefinition(state, definition) {
     state.currentModel.Definition.push(definition);
    }
  },
  getters:{
    definitions(state){
      return state.currentModel.Definition
    }
  }
});

In your component, you can add a computed prop that gets the data from store

Vue.component('report-summary', {
  props: ['translation', 'store'],
  computed: {
    definitions(){
     return store.getters.definitions
    }
  },
  template: '<div>
    '<div class="alert alert-secondary" role="alert">' +
    '<h5 class="border-bottom border-dark"> {{ translation.currently_defined }}</h5>' +
    '<div>{{ definitions.length }} {{translation.elements }}</div>' +
    '</div>' +
    '</div>'
});

UPDATE (30 Nov, 2019) If the above code still does not work, change your storeNewModelDefinition mutation to this:

storeNewModelDefinition(state, definition) {
     // Create a new copy of definitions
     let definitions = [...state.currentModel.Definition]

     // Push the new item
     definitions.push(definition)

     // Use `Vue.set` so that Vuejs reacts to the change
     Vue.set(state.currentModel, 'Definition', definitions);
    }

Upvotes: 2

Michal Lev&#253;
Michal Lev&#253;

Reputation: 37793

Just a note on your EventBus experiment (you should definitely try to fix the reactivity issue instead of trying tricks like $forceUpdate)

Reason why your experiment with EventBus didn't work is the use of anonymous function as an event handler. Take a look at this piece of code:

  methods: {
    onEvent() {
      console.log(this)
    }
  },
  mounted() {
    // this.onEvent is method where "this" is bound to current Vue instance
    this.$bus.$on("test", this.onEvent);
    // Handler is anonymous function - "this" refers to EventBus Vue instance
    this.$bus.$on("test", function() {
      console.log(this)
    });
  }
  • 1t event handler is a function where this is is explicitly (by Vue itself) bound to Vue instance where the handler "lives"
  • 2nd event handler is anonymous function, this is not explicitly bound so it ends up like this == event bus Vue instance. You can work around that using closure like this:
  mounted() {
    let self = this;
    this.$bus.$on("test", function() {
       // self = "this" in context of function "mounted"
       console.log(self)
    });
  }

this in JS can be tricky...

Upvotes: 2

Related Questions