2

I am facing an issue where I have some template HTML in a component that relies on the computed getter of a Vuex method. As you can see in the template, I am simply trying to show the output of the computed property in a <p> tag with {{ getNumSets }}.

As I update the state with the UPDATE_EXERCISE_SETS mutation, I can see in the Vue devtools that the state is updated correctly, but the change is not reflected in the <p> {{ getNumSets }} </p> portion.

Template HTML:

<template>
...
<v-text-field
   v-model="getNumSets"
   placeholder="S"
   type="number"
   outlined
   dense
></v-text-field>
<p>{{ getNumSets }}</p>
...
</template>

Component Logic:

<script>
...
computed: {
   getNumSets: {
      get() {
         var numSets = this.$store.getters['designer/getNumSetsForExercise']({id: this.id, parent: this.parent})
         return numSets
      },
      set(value) {  // This correctly updates the state as seen in the Vue DevTools
        this.$store.commit('designer/UPDATE_EXERCISE_SETS', {
                    id: this.exerciseId,
                    parentName: this.parent,
                    numSets: parseInt(value),
                    date: this.date
                })
      }

}
...
</script>

Vuex Store Logic:

...
state: {
  designerBucket: []
},
getters: {
  getNumSetsForExercise: (state) => (payload) => {
    var numSets = 0
    for (var i = 0; i < state.designerBucket.length; i++) {
      if (state.designerBucket[i].id == payload.id) {
        numSets = state.designerBucket[i].numSets
      }
    }
    return numSets
  }
},
mutations: {
  UPDATE_EXERCISE_SETS(state, payload) {
    state.designerBucket.forEach(exercise => {
       if (exercise.id == payload.id) {
          exercise.numSets = payload.numSets
       }
    })
   }
}

Any insight is very appreciated!

P.S. I have also tried using a for (var i=0...) loop, looping over the indices and then using Vue.set() to set the value. This did update the value in the store as well, but the computed property is still not updating the template.

Cameron Rosier
  • 223
  • 3
  • 15
  • In your Vuex getter, are you trying to pass an argument to the getter with that syntax? [Getters don't take arguments](https://stackoverflow.com/questions/41503527/vuexjs-getter-with-argument). – zcoop98 Jun 28 '21 at 17:49
  • 1
    @zcoop98 I actually forgot to put the correct parameters here in the question, but I edited it to reflect what it actually looks like. Sorry! – Cameron Rosier Jun 28 '21 at 19:16

2 Answers2

1

This turned into a bit of a long-winded answer, but bear with me.

Here's my hunch: since you're returning a function from your Vuex getter, Vue isn't updating your computed property on state changes because the function never changes, even if the value returned from it would. This is foiling the caching mechanism for computed properties.


Reactivity for Arrow Function Getters


One of the things to keep in mind when creating a getter like this, where you return an arrow function:

getNumSetsForExercise: (state) => (payload) => {
    var numSets = 0
    for (var i = 0; i < state.designerBucket.length; i++) {
        if (state.designerBucket[i].id == payload.id) {
        numSets = state.designerBucket[i].numSets
        }
    }
    return numSets
}

...is that you're no longer returning actual state data from your getter.

This is great when you're using it to pull something from state that depends on data that's local to your component, because we don't need Vue to detect a change, we just need the function to access current state, which it does fine.

BUT, it may also lead to the trap of thinking that updating state should update the getter, when it actually doesn't. This is really only important when we try to use this getter in a computed property like you have in the example, due to how computed properties track their dependencies and cache data.

Computed Caching and Dependency Detection


In Vue, computed properties are smarter than they first seem. They cache their results, and they register and track the reactive values they depend on to know when to invalidate that cache.

As soon as Vue calculates the value of a computed property, it stores it internally, so that if you call the property again without changing dependencies, the property can return the cached value instead of recalculating.

The key here for your case is the dependency detection– your getter has three dependencies that Vue detects:

get() {
    var numSets = this.$store.getters['designer/getNumSetsForExercise']({id: this.id, parent: this.parent})
    return numSets
},
  1. The getter: this.$store.getters['designer/getNumSetsForExercise']
  2. this.id
  3. this.parent

None of these values change when <v-text-field> calls your setter.

This means that Vue isn't detecting any dependency changes, and it's returning the cached data instead of recalculating.

How to Fix it?


Usually, when you run into these sorts of dependency issues, it's because the design of the state could be improved, whether by moving more data into state, or by restructuring it in some way.

In this case, unless you absolutely need designerBucket to be an array for ordering purposes, I'd suggest making it an object instead, where each set is stored by id. This would simplify the implementation by removing loops, and remove the need for your getter altogether:

...
state: {
  designerBucket: {}
},
mutations: {
  UPDATE_EXERCISE_SETS(state, payload) {
    // Need to use $set since we're adding a new property to the object
    Vue.set(state.designerBucket, payload.id, payload.numSets);
  }
}

Now, instead of invoking a getter, just pull designerBucket from state and access by this.id directly:

<script>
...
computed: {
    getNumSets: {
    get() {
        return this.$store.state.designerBucket[this.id];
    },
    set(value) {
        this.$store.commit('designer/UPDATE_EXERCISE_SETS', {
            id: this.exerciseId,
            parentName: this.parent,
            numSets: parseInt(value),
            date: this.date
        });
    }
}
...
</script>

This should allow Vue to detect changes correctly now, and prevent the stale cache problem from before.

zcoop98
  • 2,590
  • 1
  • 18
  • 31
  • 1
    Fantastic answer, I took your approach and it worked! Thank you @zcoop98, I accepted your answer. Also, thank you for learning me something – Cameron Rosier Jun 30 '21 at 22:07
0

Edited: First import mapGetters from 'vuex' like this on the top of the script tag.

import { mapGetters } from "vuex"

Now in your computed object, add mapGetters and pass arguments to the getter method inside the get() method like this:-

computed: {
   ...mapGetters('designer',['getNumSetsForExercise']),
   getNumSets: {
      get() {
         var numSets = this.getNumSetsForExercise({id: this.id, parent: this.parent})
         return numSets
      },
      set(value) {  // This correctly updates the state as seen in the Vue DevTools
        this.$store.commit('designer/UPDATE_EXERCISE_SETS', {
                    id: this.exerciseId,
                    parentName: this.parent,
                    numSets: parseInt(value),
                    date: this.date
                })
      }

}

And see if it works.

Zaeem Khaliq
  • 296
  • 5
  • 14