6
votes

I have normalized my data in a Vuex app but I haven't been able to find an answer on how to watch the data for changes. For example, here's a simplified version of my setup.

State:

const state = {
    shapes: {
        collection: [1, 2, 3],

        data: {
            1: {
                type: 'circle',
                isActive: false,
            },
        },
    }
}

Getters:

const getters = {
    findShape: (state) => (id) => {
        return state.shapes.data[id];
    }

    allShapes: (state, getters) => {
        return state.shapes.collection.map(id => getters.findShape(id));
    }
}

In my component I'm calling the allShapes getter in a computed property and passing the shapes down to child components via a v-for.

computed: {
    shapes() {
        return this.$store.getters.allShapes;
    }
}

Adding and removing shapes works fine. The problem is whenever I update a property in the data object the change isn't reflected in my components. Here is a mutation where I'm setting an isActive property:

const mutations = {
    setActive(state, shape) {
        Vue.set(state.shapes.data[shape.id], 'isActive', shape.isActive);
    }
}

Any component that then references the isActive property doesn't get updated. I get why this is happening but I haven't found a solution other than going back to a nested array structure. Is there a way to trigger an update in the allShapes getter when the data object changes? Or is there a different way to approach pulling the data into my components to make it reactive?

1

1 Answers

5
votes

First, a little background on Vue.set.

Calling Vue.set does two things:

  1. If the property doesn't already exist it will add it as a reactive property.
  2. The property value will be updated.

There's an edge case here. If the property already exists but isn't reactive then using Vue.set won't add reactivity.

By default the properties of state will be reactive and this reactivity will be applied recursively to all objects and arrays inside that state. However, the usual caveats apply around the addition of new properties. Adding a new property can't be detected by Vue so it won't be reactive. This is the use case for Vue.set, adding a new property.

Most likely you're loading your data from a server to populate the data object within your state. This is the critical point when you need to make things reactive. By the time you come to update the isActive property it may be too late.

Chances are you have a mutation that looks something like this:

addShape (state, shape) {
  state.shapes.data[shape.id] = shape
}

This is the point where reactivity falls down because you're adding a new property to state.shapes.data. The result will be that the shape object doesn't get processed by the reactivity system so none of its properties will be reactive.

If the isActive property doesn't exist then you might actually get away with this. In that scenario, using Vue.set as you have will find the property is missing and add it, including reactivity. It will actually do even more than that. As the object isn't already reactive it will go through the entire object and make all of the properties reactive. The only thing left unreactive in that scenario is the relevant property on state.shapes.data.

However, it seems that in your case the property isActive does exist from the outset. The result is that Vue.set doesn't help when changing isActive. Instead you need to use it when adding the object to the cache in the first place. e.g.:

addShape (state, shape) {
  Vue.set(state.shapes.data, shape.id, shape)
}

Obviously I've made a few assumptions about how you're populating the cache state.shapes.data but even if your code isn't exactly like I've described it will probably be similar enough for the same fix to apply.

So long as the property isActive is present in the shape objects from the outset it shouldn't be necessary to set isActive using Vue.set. Using it to add shapes to the cache should be sufficient.