Search code examples
javascriptvue.jscomputed-properties

Computed property in Vue not calculating


I have a ecommerce app (SPA) built in Vue and below is the snapshot of the store

state: {
    cart: [],
    totalItems: {
      count: 0
    }
},
getters: {
    totalItems(){
      if(window.localStorage.totalItems){
        return JSON.parse(window.localStorage.totalItems)
      }
      else{
        let totalItems = { count: 0}
        return totalItems
      }
    }
},
mutations: {
    setCart(state, cart){
      state.cart = cart
      window.localStorage.cart = JSON.stringify(cart)
    },
    setTotalItems(state, totalItems){
      state.totalItems = totalItems
      window.localStorage.totalItems = JSON.stringify(totalItems)
    }
},
actions: {
    addToCart({ commit, getters }, productId){
      let cart = getters.cart
      let totalItems = getters.totalItems
      if(cart.length == 0){
        cart.push({id: productId, quantity: 1})
        totalItems.count++
      }
      else if(cart.find(({ id }) => id == productId)){
        let item = cart.find(({ id }) => id == productId)
        item.quantity++
        totalItems.count++
      }
      else{
        cart.push({id: productId, quantity: 1})
        totalItems.count++
       }
       commit('setCart', cart)
       commit('setTotalItems', totalItems)
    },
    setTotalItems({ commit }, totalItems){
      commit('setTotalItems', totalItems)
    }
}

In my App.vue file is below -

<template>
  <v-app>
    <v-app-bar
      app
      color="red"
      dark
    >
      <v-btn text to="/">Vue Shop</v-btn>

      <v-spacer></v-spacer>

      <v-btn text to="/cart">
        <v-badge v-if="totalItems.count" :content="totalItems.count"><v-icon>mdi-cart</v-icon></v-badge>
      </v-btn>

    </v-app-bar>

    <v-main>
      <router-view></router-view>
    </v-main>
  </v-app>
</template>

<script>


export default {
  name: 'App',
  components: {
  },
  computed: {
    totalItems(){
      return this.$store.getters.totalItems
    }
  }
};
</script>

When I load the site I can see that the computed property calculates. However when I click on the 'Add to' button on the Home.vue file shown below it is supposed to

  1. Call the addToCart method
  2. Which in turn dispatches the addToCart action from the store
  3. Where I am calculating the totalItems and setting the value using the setTotalItems mutation

The issue I am facing is when I click on the 'Add to' button I can see that the value for totalItems in updated in localStorage but it is not reflecting in the v-app-bar component. It only does so if I navigate to a different page and then come back to the main page.

When I implemented by storing the value in the state instead of localStorage it reflects correctly, without having to navigate to a different page.

Is there a way to achieve this while still using localStorage instead of the store

<template>
      <v-container>
        <v-row>
          <v-col v-for="product in products" :key="product.id">
            <v-card width="300" height="300">
              <v-img :src=product.imgUrl width="300" height="200"></v-img>
              <v-card-title>
                {{ product.name }}
              </v-card-title>
              <v-card-text>
                ${{ product.price }}
                <v-btn small class="ml-16 primary" @click="addToCart(product.id)">Add to <v-icon>mdi-cart</v-icon></v-btn>
              </v-card-text>
            </v-card>
          </v-col>
        </v-row>
      </v-container>
    </template>
    
    <script>
    
    
    export default {
      name: 'Home',
      components: {
      },
      computed: {
        products(){
          return this.$store.getters.products
        },
        cart(){
          return this.$store.getters.cart
        }
      },
      methods: {
        addToCart(id){
          this.$store.dispatch('addToCart', id)
        }
      }
    }
    </script>

Solution

  • Problem Local Storage is not reactive, therefore your getter won't ever reevaluate.


    Solution You could rewrite your getter so it only retrieves the value from the local storage if the state hasn't yet been initialized. Subsequent calls should then directly access the state, since you're changing that in your mutators and that will trigger a reevaluation of your getter and therefore your computed property:

    getters: {
        totalItems(state){
          if(state.totalItems.count < 0){
            return JSON.parse(window.localStorage.totalItems || '{count: 0}')
          }
    
          return state.totalItems;
        }
    },
    

    You can then initialize your count with -1, so the first evaluation of the getter will look into the local storage.

    By referencing the totalItems property in your getter, vue knows to reevaluate this getter whenever this property changes.

    The downside here is, that your getter and your state won't be in sync until the first change to the count.


    You could probably also initialize your state with a call to localStorage, and eliminate it from the getter.

    state: {
       totalItems: {
          count: JSON.parse(window.localStorage.totalItems || '{count: 0}')
       }
    },
    getters: {
        totalItems(state){
          return state.totalItems;
        }
    },
    

    Upside: your state and getter are in sync, but this also makes your getter redundant.