Search code examples
javascriptvue.jsvuejs2vuex

Using Vuex, how do I remove items from an array when they are part of an array of objects?


Referencing the live demo code here:

https://codesandbox.io/s/vue-template-r26tg

Let's say I have a Vuex store with the following data:

const store = new Vuex.Store({
  state: {
    categories: [
      {
        name: "Category A",
        items: [{ name: "Item 1" }, { name: "Item 2" }, { name: "Item 3" }]
      },
      {
        name: "Category B",
        items: [{ name: "Item A" }, { name: "Item B" }, { name: "Item C" }]
      },
      {
        name: "Category C",
        items: [{ name: "Item !" }, { name: "Item @" }, { name: "Item #" }]
      }
    ]
  }
});

And I have an App.vue, Category.vue and Item.vue that are set up so that they are rendered like so:

//App.vue
<template>
  <div id="app">
    <Category v-for="(category, index) in categories" :category="category" :key="index"/>
  </div>
</template>

<script>
  export default {
    components: { Category },

    computed: {
      ...mapState(["categories"])
    }
  };
</script>
//Category.vue
<template>
  <div class="category">
    <div class="header">{{ category.name }}</div>
    <Item v-for="(item, index) in category.items" :item="item" :key="index"/>
  </div>
</template>

<script>
  export default {
    components: { Item },

    props: {
      category: { type: Object, required: true }
    }
  };
</script>
//Item.vue
<template>
  <div class="item">
    <div class="name">{{ item.name }}</div>
    <div class="delete" @click="onDelete">&#10006;</div>
  </div>
</template>

<script>
  export default {
    props: {
      item: { type: Object, required: true }
    },

    methods: {
      onDelete() {
        this.$store.commit("deleteItem", this.item);
      }
    }
  };
</script>

In other words, App.vue gets the list of categories from Vuex, then passes it down to Category.vue as a prop for each category, then Category.vue passes down category.items to Item.vue as a prop for each item.

I need to delete an item when the delete button next to it is clicked:

enter image description here

However, at the Item.vue level, I only have access to the item, but not the category. If I send the item to Vuex, I have no way of telling which category it belongs to. How do I get a reference to the category so that I can delete the item from it using Vuex?

I can think of two ways:

  1. Add a parent reference back to the category for each item. This is undesirable not only because I'd have to massage the item data, but also because it introduces a circular reference that I'd rather not have to deal with in other parts of the app.

  2. Emit an event from Item.vue up to Category.vue and let Category.vue handle the Vuex call for deletion. This way the category and the to-be-deleted item are both known.

Is there a better way of handling this kind of deletion?


Solution

  • I'd strongly recommend (2). In general, if you can create a component which takes props and emits events without having other side effects (API calls, Vuex mutations, etc.) that's usually the correct path. In this case, you can probably even push the event all the way back to the parent's parent.

    Where shared state (Vuex) really helps is when you have two or more components which are far away from each other in the DOM tree. E.g. imagine a header with a count of the total items. That degree of spatial separation may exist in your app, but it doesn't in this simple example.

    An additional benefit to emitting an event here is that you care more easily use tools like storybook without having to deal with any Vuex workarounds.