beforeUpdate/updated not triggered on component when slot content in child component changes
Version
3.2.19
Reproduction link
Steps to reproduce
- Note that
NoSlot.vue
andWithSlot.vue
are identical aside from the use of a slot component inWithSlot.vue
- Click the "Add value" button
- Observe the output of both components
What is expected?
Both the no-slot and with-slot components should output the same result. The onBeforeUpdate
and onUpdated
triggers fire on both components.
What is actually happening?
The no-slot component behaves as expected, but the with-slot component is never firing its onBeforeUpdate
hook, causing the ref array to never get emptied.
I am migrating a code base from Vue 2. The application makes heavy use of array refs and slots, and I would have expected the solution provided in the migration guide to give me the same behaviour as Vue 2. However, beforeUpdate
is not called on the parent component when the slot content changes, even though the template belongs to the parent component and it is rendered in the scope of the parent component. This means the the array ref is never being emptied and continues to grow with each update.
If this is expected behaviour, then a method to get an array ref when the child elements are inside a slotted component should be added to the documentation or migration guide.
This is expected behavior in Vue 3 since the parent component in this case did not update, which is a form of optimization.
A workaround here is using the @vnode-updated="updateRefs"
event on a container element inside the slot instead.
Thank you, the workaround is suitable for my use case. I understand the need for the optimisation, but the behaviour seems unintuitive to me - it looks like the component is being updated (ref callbacks are being called in the component's scope), and yet the update hooks aren't called on that component. In my mind the component is being updated, the fact that the content is in a slot (and therefore inside a child component) is something I shouldn't need to worry about.
I don't think it's too uncommon to have refs within slotted content. Here's a basic example using Vuetify which has a number of container components such as <v-row>
and <v-col>
. Having to worry about the scope of the slot and vnodes makes it a lot harder to use components and refs. Is there some solution that can be added to the framework to make managing this pattern easier? At the very least I think this should be noted in the docs and the migration guide as something which could catch people out. I'm happy for this issue to be closed if you don't agree.
The slot behavior that you find unintuitive exists in order to improve on one of the more common perf problems in Vue 2 applications: slots updates forcing a chain of ancestor components to re-render even though only the slot content is affected.
Bu running the slot rendering in the child component, instead of the parent, we can collect reactive dependencies in the slot as dependencies of the child's template instead of the parent's, meaning the child can re-render independently when reactive data (only) in the slot changed.
We will definitely keep this behavior (which already existed in the form of "scoped slots" in Vue 2).
We can consider how to make refs on v-for more ergonomic, and yes, at least document the vnode hook pattern in the docs.
@yyx990803 wouldn't a vnode hook on the component itself also work, or are there edge cases that I#m missing?
<template>
<slot-comp @vnode-before-update="refs.length = 0">
<h3>{{name}}</h3>
<div v-for="v in store.vals" :ref="(r) => { if (r) refs.push(r); }">Component #{{v}}</div>
</slot-comp>
</template>
@LinusBorg
I'd also document the need to hoist beforeUpdate
or employ some generic slot wrapper in nested situations.
For example, the following will not work as the @vnode-before-update
listener in app
only reaches down one level.
slot-a
<template>
<slot/>
</template>
slot-b
<template>
<slot-a>
<slot/>
</slot-a>
</template>
app
<template>
<slot-b @vnode-before-update="clearItemRefs">
<span v-for="item in list" :ref="setItemRef">{{ item }}</span>
</slot-b>
</template>
You'd need to hoist beforeUpdate
, like:
slot-a
<script>
export default {
emits: [ 'beforeUpdate' ],
beforeUpdate() {
this.$emit( 'beforeUpdate' );
};
};
</script>
<template>
<slot/>
</template>
app
<template>
<slot-b @beforeUpdate="clearItemRefs">
<span v-for="item in list" :ref="setItemRef">{{ item }}</span>
</slot-b>
</template>
Or, construct a generic wrapper to capture updates:
slot-events
<script>
export default {
emits: [ 'beforeUpdate' ],
beforeUpdate() {
this.$emit( 'beforeUpdate' );
};
};
</script>
<template>
<slot/>
</template>
app
<template>
<slot-b>
<slot-events @beforeUpdate="clearItemRefs">
<span v-for="item in list" :ref="setItemRef">{{ item }}</span>
</slot-events>
</slot-b>
</template>
As I build slotted components derived from others, I'm using the latter pattern to maintain composability.
I don't want to preemptively guess where I need to hoist.
Or, rely on the happenstance of an immediate parent where @vnode-before-update
would suffice.
Although, this complicates styling.
:slotted
becomes ineffective and I'm forced to use :deep
with a child combinator to mimic the behavior.
It looks like support for array refs was added back in with commit 41c18effea9dd32ab899b5de3bb0513abdb52ee4, which fixes the difficulties with array refs as it's no longer necessary to use function refs for them. Closing this issue as the slot updated
triggering is intended behaviour and there is now a good solution for refs in slotted content.