Vuex - Update an entire array
Asked Answered
I

5

18

I have a Vue.js app. This app is using Vuex for state management. My store looks like this:

const store = new Vuex.Store({
    state: {
        items: []
    },

    mutations: {
        MUTATE_ITEMS: (state, items) => {
            state.items = items;
        }
    },

    actions: {
        loadItems: (context, items) => {
            context.commit('MUTATE_ITEMS', items);
        }
    }
  })
;

In my Vue instance, I have the following method:

loadItems() {

  let items = [];
  for (let I=0; I<10; I++) {
    items.push({ id:(I+1), name: 'Item #' + (I+1) });
  }
  this.$store.dispatch('loadItems', items);
},

When I run this, I notice that the item list in my child components are not getting updated. I suspect this is because of the reactivity model in Vue.js. However, I'm not sure how to update an entire array. In addition, I'm not sure if I need to use Vue.set in my store mutation, store action, or in the Vue instance method itself. I'm slightly confused.

Component:

<template>
    <div>
        <h1>Items ({{ items.length }})</h1>
        <table>
            <tbody>
                <tr v-for="item in items" :key="item.id">
                    <td>{{ item.id }}</td>
                </tr>
            </tbody>
        </table>
    </div>
</template>

<script>
    import { mapState } from 'vuex';

    export default {
        computed: mapState({
            items: state => state.items
        })
    };
</script>

How do I update an entire Array that is centrally stored in Vuex in a Vue.js app?

Indopacific answered 8/6, 2018 at 19:21 Comment(3)
i think it's more an issue about how you render the store and the component because your mutation actually works: example hereTwilatwilight
@sovalina Thank you for sharing that tool. I had never seen that before. I loaded my entire project here. Please note, I did not know how to import the components in the main.js file though. My actual code uses require statements. I feel like its so close. I'm just not sure what I'm doing wrong.Indopacific
instead of require you can use import in your router (you can check this post to see the difference) : fixed codeSandboxTwilatwilight
J
37

use vue's set function. This will make sure that the Vue's reactivity kicks in and updates the required objects.

import Vuex from 'vuex';
const store = new Vuex.Store({
    state: {
        items: []
    },

    mutations: {
        MUTATE_ITEMS: (state, items) => {
            Vue.set(state, 'items', items);
            // or, better yet create a copy of the array
            Vue.set(state, 'items', [...items]);
        }
    },

    actions: {
        loadItems: (context, items) => {
            context.commit('MUTATE_ITEMS', items);
        }
    }
  })
;

When dealing with arrays or Objects, it's a good idea to prevent mutability, which I usually do with a spread operator {...myObject} or [..myArray] this will prevent changes to the object from other source to change your source, so it's a good idea to implement in getters too.


Update:

Here is a working example: https://codesandbox.io/s/54on2mpkn (codesandbox allows you to have single file components)

The thing I noticed is that you don't have any getters, those help get the data. You can call them using computed values directly, or using mapGetters. but they are not mandatory. Here are three ways you can get the data

<script>
import { mapGetters } from "vuex";
import Item from "./Item";

export default {
  name: "ItemList",
  components: {
    Item
  },
  computed: {
    ...mapGetters(["items"]), // <-- using mapGetters
    itemsGet() {    // <-- using getter, but not mapGetters
      return this.$store.getters.items;
    },
    itemsDirect() {  // <--no getter, just store listener
      return this.$store.state.items;
    }
  }
};
</script>

it doesn't matter which one you chose from the functionality standpoint, but using getters makes for more maintainable code.

Joselyn answered 8/6, 2018 at 20:11 Comment(4)
Thank you for your response. I tried both approaches. However, neither worked. I added a console.log(items); just before the Vue.set(…) call. That confirmed that the items array existed as expected. However, it's still not updating the data in the child component.Indopacific
can you share the code of how you call the data in your child? I suspect you may be looking for changes in an item within the items arrayJoselyn
Thank you for your willingness to help. I've created a JSFiddle here. Notably, I could actually get my single file component in the Fiddle, so I added to the question itself above. Once again, thank you for your help.Indopacific
It's not work with mapGetters: Vue.set(state, 'items', items);. It's works: Vue.set(state, 'items', [...items]) or state.items = [...items], do not ask me why.Yocum
P
5

You need to dispatch action like any of the following:

// dispatch with a payload
this.$store.dispatch('loadItems', {items})

// dispatch with an object
this.$store.dispatch({type: 'loadItems',items})
Pecan answered 8/6, 2018 at 19:28 Comment(0)
P
1

If you've come to this answer in 2021 and are using Vuex 4, Vue 3, Typescript, the setup() function (also referred to as the Composition API) the following is how i solved a similar problem.

Initially I was mutating an array in the vuex state via an async action and not seeing UI changes propogate. So here's the pretense.

I have a vuex state like this

interface State {
  areas: Area[]
}

a component like this

export default {
   name: "Area",
   setup() {
      const store = UseStore();
      store.dispatch('fetchAreas');
      const areas = store.state.areas;
      return {
        store,
        areas,
      }
   }
}

the dispatch function for that action is something like this

async ['fetchAreas](context: ContextType<State>): Promise<void> {
   const areas = await getTheStuff();
   context.commit('setAreas', areas);
} 

Thats the pretense

My problem was in the mutation receiver for 'setAreas'

Originally it looked like this

['setAreas'](state: State, value: Area[]) {
  state.areas = value;
}

And that failed to trigger a ui update. I changed it to this

['setAreas'](state: State, value: Area[]) {
   state.areas.splice(0, state.areas.length);
   value.forEach(v => state.areas.push(v));
}

That triggered a UI update. Why?

I believe that by originally overriding the entire array, vuex change detection via reactivity lost the reference to the original array and so the component so no changes while vuex re-registered the new array to be reactive.

The second approach utilizes array methods, to keep the original array reference in place, but update its elements entirely. It's not efficient, but it works, and sometimes, thats all we can hope for.

Presignify answered 4/8, 2021 at 15:20 Comment(0)
A
0

Virtually none of the above solutions worked for me (Vue2/Vuex3), however the below mutation works exactly as needed: the array is replaced, and reactivity is preserved.

I was confused why Vue.set was not working for me - it would update the state but not trigger an update to the view.

So instead the below was just following the docs as to which actions would trigger updates.

https://v2.vuejs.org/v2/guide/list.html#Mutation-Methods

SWAP_DATASET(state, dataset) {
    if (state.currentDataSets.length > 0) {
      state.currentDataSets.shift()
      state.currentDataSets.push(dataset)
    } else {
      state.currentDataSets.push(dataset)
    }
  },
Aposematic answered 22/10, 2021 at 4:49 Comment(0)
P
-1

I had a problem like that and I solved like below: // in the component

computed: {

 data: {
     get() {
         this.$store.commit('updateData', this.$store.state.data)
         return this.$store.state.data;
     },
 }

} // in the store

 mutations: {
      updateData(state,item){
          store.state.data = item;
      }
  }

yes, it's very amazing.

Perspiration answered 3/12, 2020 at 21:42 Comment(1)
I am sorry, but how to be sure Vue 2 reactivity will not be lost with such a reassignment?Vanward

© 2022 - 2024 — McMap. All rights reserved.