Search code examples
javascriptvue.jsvuejs2vuex

Which method of adding an element to the array property of a Vuex state property correct?


So I have an action that makes a POST request to an endpoint that creates a comment for a particular artwork. On the components which renders the artwork and its comments, I dispatch an action in the onMounted() hook that makes a GET request for the artwork with that id, and then stores it in the Vuex.

Once the POST request that creates the comment goes through, I can access the artwork property in the store, and just push the response to the comments property which is an array of comments. I don't know if this is the correct way to do it though, since from what I understand any state change should be done through mutations, so directly accessing the state and pushing array elements into it seems incorrect?

This is my action that creates a comment and pushes the response to the selected artwork's comments property:

    async createComment({commit, state}, payload){
        try {
            let response = await axios.post("/createComment", payload)
            console.log(response)
            state.image.comments.push(response.data.comment)
        } catch (error) {
            console.log(error)
        }
    },

I guess the alternative is to copy the artwork from the state, push the new comment in the comments property of the copy, and then commit the new object?


Solution

  • Getting an object from the state and mutating its properties violates the principle of "one source of truth" for all changes, which is why you're using a store in the first place.
    You have to replace the object in the state with a modified version of itself. Here's how a mutation might look like, in your case:

    this.$store.commit('ADD_COMMENT', comment);
    // in store:
    mutations: {
      ADD_COMMENT(state, comment) {
        state.image = { 
          ...state.image,
          comments: [ ...(state.image.comments || []), comment]
        }
      }
    }
    

    This can also be achieved by push-ing the comment into the existing comments array. But you still have to replace state.image:

    // ❌ WRONG, you're not replacing the image:
    state.image.comments.push(coment);
    
    // ✅ CORRECT, you're replacing the image:
    const clone = { ...state.image };
    clone.comments.push(comment);
    state.image = clone;
    
    // that's why most prefer the spread syntax, 
    // to assign in one line, without using a const: 
    // state.image = { ...state.image, [yourKey]: yourNewValue };
    

    Important: mutating state should only happen inside mutation functions. If it happens outside, Vue will warn you. Sometimes it might work (as in, you might actually see the change in the component and it might render correctly), but it's not guaranteed to work.


    Example updating an object in a state array:

    If the image would be part of an array stored into state, (say images) and considering each image would have an unique identifier id, here's how you could do it:

    this.$store.commit('ADD_IMAGE_COMMENT', { id: image.id, comment });
    
    // the mutation:
    mutations: {
      ADD_IMAGE_COMMENT(state, { id, comment }) {
        const index = state.images.findIndex(image => image.id === id);
        if (index > -1) {
          state.images.splice(index, 1, { 
            ...state.images[index],
            comments: [...(state.images[index].comments || []), comment]
          });
        }
      }
    }
    

    The above mutation mutates the images array in place, effectively replacing it with a new copy of itself, which contains all the other images unmodified and a new version of the modified image at the same index as the old one. The modified image contains everything the old one contained, except the comments array, replaced by a new array, containing everything the old one had, plus the new comment.