I’d like to react to changes made to an array of template ref elements created with a v-for
loop over some collection of arbitrary objects. I’d like to pass this array to a component / composable and have it react to the changes.
The docs provide a way to save these elements to an array: https://vuejs.org/guide/essentials/template-refs.html#refs-inside-v-for
I’ve modified the provided example to see, if the resulting itemsRef
array would be reactive. The code is:
<script setup>
import { ref, useTemplateRef, watch, markRaw } from 'vue'
const list = ref([1, 2, 3])
const itemRefs = useTemplateRef('items')
watch(itemRefs, () => {
console.log("itemsRef:", itemRefs.value.length);
}, {deep: true});
watch(list, () => {
console.log("list:", list.value.length, itemRefs.value.length);
}, { deep: true });
</script>
<template>
<button @click="list.push(list.length + 1)">add</button>
<ul>
<li v-for="item in list" ref="items">
{{ item }}
</li>
</ul>
</template>
But the return value of useTemplateRef
seems to be a ShallowRef
, judging from its TypeScript signature:
function useTemplateRef<T>(key: string): Readonly<ShallowRef<T | null>>
So the watch
on itemRefs
really runs just once:
As you can see, array’s contents are changing (list
is updated), but the changes to itemRefs
are not captured by vue. I tried making the watch
deep, but it does not help.
Is there a way to make the ref not shallow somehow? Or maybe there is another way?
const { createApp, ref, useTemplateRef, watch, markRaw } = Vue;
const app = createApp({
setup() {
const list = ref([1, 2, 3]);
const itemRefs = useTemplateRef('items');
watch(itemRefs, () => {
console.log("itemsRef: ", itemRefs.value.length);
}, { deep: true });
watch(list, () => {
console.log("list: ", list.value.length, itemRefs.value.length);
}, { deep: true });
return { list }
}
});
app.mount('#app');
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<div id="app">
<button @click="list.push(list.length + 1)">add</button>
<ul>
<li v-for="item in list" ref="items">
{{ item }}
</li>
</ul>
</div>
2
Answers
Using
const elements = ref([])
withref="elements"
inside av-for
does not guarantee order, as per vue's docs.Also, if your
ref="elements"
is a Component, theelements
array will be full ofComponentPublicInstance
elements.To battle this, I used this gist as the base and created this composable:
Which can be used this way:
Now the
messageRefs
array is fully reactive and is in full sync withmessages
array.itemRefs
is updated (e.g reactive). But it’s always updated 1 render cycle afterlist
has changed.Wrap your
console.log
innextTick
(imported from ‘vue’) and the numbers will match:Also note you can’t watch it deeply, since it’s a shallow ref. To be able to get this functionality, use a normal
ref
: