DefineTemplate / ReuseTemplate - Slotted content is incorrectly used between instances
matthew-dean opened this issue · comments
Describe the bug
If <DefineTemplate>
has a slot, this slot will be re-used between component instances.
Reproduction
https://stackblitz.com/edit/vitejs-vite-hrgeva?file=src%2FApp.vue,src%2FMyComponent.vue
System Info
System:
OS: Linux 5.0 undefined
CPU: (8) x64 Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
Memory: 0 Bytes / 0 Bytes
Shell: 1.0 - /bin/jsh
Binaries:
Node: 18.18.0 - /usr/local/bin/node
Yarn: 1.22.19 - /usr/local/bin/yarn
npm: 10.2.3 - /usr/local/bin/npm
pnpm: 8.15.3 - /usr/local/bin/pnpm
npmPackages:
@vueuse/core: ^10.9.0 => 10.9.0
vue: ^3.2.25 => 3.2.31
Used Package Manager
npm
Validations
- Follow our Code of Conduct
- Read the Contributing Guidelines.
- Read the docs.
- Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.
- Make sure this is a VueUse issue and not a framework-specific issue. For example, if it's a Vue SFC related bug, it should likely be reported to https://github.com/vuejs/core instead.
- Check that this is a concrete bug. For Q&A open a GitHub Discussion.
- The provided reproduction is a minimal reproducible example of the bug.
So, I think the long and the short is that documentation is currently wrong (or this is a legit bug). It says:
When using with Options API, you will need to define createReusableTemplate outside of the component setup and pass to the components option in order to use them in the template.
This seems to work only if your template is not bound to the state of the current instance, as it's defined outside of the setup process.
Here is the corrected code (using setup properly, similar to <script setup>
) that should be in the example:
<script>
import { defineComponent } from 'vue'
import { createReusableTemplate } from '@vueuse/core'
export default defineComponent({
setup() {
const [DefineTemplate, ReuseTemplate] = createReusableTemplate()
return { DefineTemplate, ReuseTemplate }
},
})
</script>
<template>
<component :is="DefineTemplate" v-slot="{ data, msg, anything }">
<div>{{ data }} passed from usage</div>
</component>
<component :is="ReuseTemplate" :data="data" msg="The first usage" />
</template>
That said, maybe there's another way to pass in components that would correctly re-use slots from the parent? 🤷♂️ It's possible that these components are just defined incorrectly, and are somehow memo-izing what's being passed into the slot? I feel like other components passed to the Options API handle slotted content correctly, but DefineTemplate
is effectively already "re-using" content so I wonder if that re-use logic is just wrong.
Oh yeah! I see the issue!
createReusableTemplate
creates a ref on this line.DefineTemplate
sets the render value on this line.ReuseTemplate
does nothing except return the value ofrender.value
on this line.
Therefore, by calling createReusableTemplate
outside of the setup function, it creates one instance of render.value
across all instances (that are using the Options API).
The way you could make this work according to the example, might be something like the following:
- Create a WeakMap in
createReusableTemplate
that maps instances to render values - In
DefineTemplate
's setup function, create a new render value and use something likegetCurrentInstance
to get the component parent and set that in the WeakMap. - In
ReuseTemplate
, lookup the WeakMap using again the parent instance and return the render value.