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">✖</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:
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:
Add a parent reference back to the
categoryfor eachitem. This is undesirable not only because I'd have to massage theitemdata, but also because it introduces a circular reference that I'd rather not have to deal with in other parts of the app.Emit an event from
Item.vueup toCategory.vueand letCategory.vuehandle 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
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.
Comments
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)">✖</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
Category.vue: :on-delete="deleteItem(item)", which makes it even closer to what the event option would be.