3

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?

2 Answers 2

1

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.

Sign up to request clarification or add additional context in comments.

Comments

0

Personally, I'd go with 2 (emit an event from Item.vue up to Category.vue), but, since you asked about possibilities, there is a third way: passing a callback function.

Example:

Category.vue:

<template>
  <div class="category">
    <div class="header">{{ category.name }}</div>
    <Item v-for="(item, index) in category.items" :item="item" :key="index"
     :on-delete="deleteItem"/>
  </div>
</template>

<script>
// ...
export default {
  // ...
  methods: {
    deleteItem(i) {
      console.log('cat', this.category.name, 'item', i)
      //this.$store.commit("deleteItem", this.item);
    }
  }
};
</script>

Item.vue:

<template>
  <div class="item">
    <div class="name">{{ item.name }}</div>
    <div class="delete" @click="() => onDelete(this.item)">&#10006;</div>
  </div>
</template>

<script>
export default {
  props: {
    item: { type: Object, required: true },
    onDelete: { type: Function }
  },
};
</script>

Updated sandbox here. Notice, in this case, the callback is onDelete.

If this were React, the callback was for sure a more idiomatic way. In Vue, as said, I'd argue in favor of emitting the event in the child and handling it in the parent (with v-on).

1 Comment

Of course, you can also declare, in Category.vue: :on-delete="deleteItem(item)", which makes it even closer to what the event option would be.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.