Subscribe on changes!

`onBeforeUnmount` weird behaviour if the the custom component is on the same DOM tree level.

avatar
Dec 2nd 2023

Vue version

3.3.8

Link to minimal reproduction

https://stackblitz.com/edit/vue3-script-setup-with-vite-l1j4xb?file=src%2FApp.vue

Steps to reproduce

This problem could me my missing knowledge how vnodes and lifecycle hooks working under the hood, but this behaviour is a little bit odd for me because it's working differently with a fresh vite create app and on stackblitz but works differently on SFC Playground.

This is the behaviour on my local env (also on stackblitz): https://github.com/mateenagy/vue-weird-rerender

  • Create a new project with npm create vite@latest my-vue-app -- --template vue-ts
  • Create a simple component with some prop and add onBeforeUnmount hook with logging out the props.name for example.
  • Use the created component under each other like this:
<div>
    <Test name="foo" />
    <Test name="bar" />
<div>
  • Open console in browser then delete the name=foo component
  • Browser console will log bar instead of foo

SFC Playground:(https://play.vuejs.org/#eNp9Uk1v2zAM/SuaLnaBwD70ljkB1qHANmBbsQ/0UPXg2rTrViYFSU4DGP7vpeQmTdu0N5GPfHwU3yi/GJNtBpBLWbjKdsYLB34wa4Vdb8h6MQoLjZhEY6kXCZcme+gr9eYpn+UhCEzJZ4UKK0LnRe9asQr9afINtCZxSVbXn5IThUU+j+NBHHjojS49cCREUXeb+OBnHIFlDyslGyIlRX4MuintHiryub/ID1jlQnrHopquze4cIe87hmIlK6bpNNjfxncsWsmliEjAStb88CPmvB1gsctXt1DdH8nfuW3IKXlhwYHdgJJ7zJe2BT/D539/wZbfe7CnetBc/QH4BxzpIWicy84GrFn2QV1U+z2epsP2nzvfekC3WyoIDZVTrFeSTxV+8L3Vn+WeZqexT+HEv7g784FhdIkt38AzwVvzEJ5BQxb+Y08D+gUnfoYH1K88tfOMsWQcu6aGpkO4CFERVYVTL4XzlpdjLeuUXaTwFX+anojVel4j8JGGTFObJleh/XopksU8IQsxM0wvvXjEieMYR4tpOu6s6REggxkW)

The behaviour in this is different because when I remove the first component then both onBeforeUnmount event triggered.

What is expected?

I leave some commented out example where it's working as expected:

When I remove the name=foo component then the onBeforeUnmount should log foo

What is actually happening?

It's actually trigger the last component onBeforeUnmount hook on the same DOM level:

<div>
    <Test name="foo" /> <--- If I remove this
    <Test name="bar" /> <--- This `onBeforeUnmount` will be triggered
</div>

This not happens in these cases:

<div>
    <Test name="foo" /> <--- If I remove this then this component `onBeforeUnmount` hook will be trigged
    <div></div>
    <Test name="bar" />
</div>
<div>
    <div>
        <Test name="foo" /> <--- If I remove this then this component `onBeforeUnmount` hook will be trigged
    </div>
    <div>
        <Test name="bar" />
    </div>
</div>
<div>
    <Test name="foo"  v-if="show"/> <--- If I remove this then this component `onBeforeUnmount` hook will be trigged
    <Test name="bar" />
</div>

System Info

No response

Any additional comments?

No response

avatar
Dec 6th 2023

I'm not sure if this is a bug, but it's all explainable. In this use case, the compiler will think that it is a stable dom structure, so it will not attach any patchFlag. However, due to the deletion of the code, the dom structure is destroyed, and during the hot update process, the render function is re-executed, so it is wrong. The dom is updated, that is, foo becomes bar, and then uninstalled.

<Comp name="foo" key = 'foo'/>
 <Comp name="bar" key = 'bar'/>
avatar
Dec 6th 2023

Yes it was on me to forget about the description on the :key property on the component in the vue docs: Without keys, Vue uses an algorithm that minimizes element movement and tries to patch/reuse elements of the same type in-place as much as possible.

And the it make perfect sense, but I just show a basic example (the stackblitz link contains the :key workaround). My use case is that I have a component which value is in a global store and if I remove it then it updates the store. The problem is In this case that If I don't know the name from the component then the I cant remove from the store properly. The key is a workaround but not really a good developer experience and I mean when I am not using v-for.

I wonder if it's possible to have the option for render function to tell the renderer to not patch/reuse that elements instead just remove it? So I don't have to use key on every singe component when I use them under each other.

Something like this:

defineComponent({
       patch: false,
       props: {
           name: {
               type: String,
               default: '',
           },
           default: {
               type: [String, Boolean, Number, Object],
               default: '',
           },
           ignore: {
               type: Boolean,
               default: false,
           },
           preserve: {
               type: Boolean,
               default: false,
           },
       },
       emits: ['update:modelValue'],
       setup: (props, ctx) => {
           return () => {
               return h(component, {
                   ...props,
                   name: props.name,
               },
               { ...ctx.slots });
           };
       },
   });