I would consider this as a fundamental question. It's about the principles of vue2/vue3 reactivity.
Suppose we have items with tags (many-to-many). They are stored in vuex like that (using normalizr approach):
export const state = () => ({
itemsOrder: [1, 2, ...],
itemsByIds: { 1: { id: 1, title: 'Item 1' }, 2: { id: 2, title: 'Item 2' } ... },
tagsByIds: { 1: { id: 1, title: 'Tag 1' }, 2: { id: 2, title: 'Tag 2'}, ... },
itemTags: [{ itemId: 1, tagId: 1 }, ...]
})
Our goal is to show items with tags. There's some <ItemWithTags>
component that has to show item title and tags, assigned to that item. And - the very important condition - we have to avoid unnecessary re-rendering as much as possible. If a tag gets assigned to an item (a new entry is created in itemTags) then only that particular <ItemWithTags>
component should receive the update.
How to do that?
We can not refer to state.itemTags
array from inside the <ItemWithTags>
because then the reference triggers every component to update when a new entry is pushed to this array. For simplicity suppose <ItemWithTags>
does not have any reference to the $store
, it uses props only.
Let's build the getter:
getters = {
itemsWithTags (state) {
return state.itemsOrder.map(id => ({
id,
item: state.itemsByIds[id],
tags: state.itemTags
.filter(itemTag => itemTag.itemId === id)
.map(itemTag => state.tagsByIds[itemTag.tagId])
})
}
}
Notice, that when any new itemTag relation is created, this getter builds a new array that consists on new objects like {id, item, tags}, each object has a reference to the same item as before and a new tags array. For all items but one the tags array is internally the same - it consists on the same tag objects in the same order. But that's a new array for each entry.
Let's iterate over itemsWithTags
in some parent component (for now just show items without tags):
<ItemWithTags
v-for="entry in itemsWithTags"
:key="entry.id"
:item="entry.item"
/>
This works correctly - a new relation triggers the getter to rebuilt, but for each entry <ItemWithTags>
receives the same entry.item
object as before. No re-rendering, everything works as it should.
We can even show the item with one first assigned tag and avoid re-rendering:
<ItemWithTags
v-for="entry in itemsWithTags"
:key="entry.id"
:item="entry.item"
:first-tag="entry.tags.length ? entry.tags[0] : null"
/>
Once again - we refer to the same entry.item
and the same entry.tags[0]
objects as before. Here's the demo.
But how to handle all the tags in a correct way? If we do:
<ItemWithTags
v-for="entry in itemsWithTags"
:key="entry.id"
:item="entry.item"
:tags="entry.tags"
/>
then every new relation triggers every <ItemWithTags>
to update. That happens because the getter constructs new entry.tags
array for each item. One more demo is here.
The only working solution I found so far is to convert array to some primitive type using JSON.stringify (the last demo), but that's so ugly and not performant, that I'am eager to find a proper way. By proper I mean - we have a getter, some dependency says that it has to update. Then the update should be more selective. It should be possible to reuse some parts of the previous getter value while building the new one.