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>
itemRefs
is updated (e.g reactive). But it's always updated 1 render cycle after list
has changed.
Wrap your console.log
in nextTick
(imported from 'vue') and the numbers will match:
const { createApp, ref, useTemplateRef, watch, nextTick } = 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,
() => {
nextTick(() =>
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>
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
:
const { createApp, ref, watch, nextTick } = Vue
const app = createApp({
setup() {
const list = ref([1, 2, 3])
const items = ref([])
watch(
items,
() => {
console.log('itemsRef: ', items.value.length)
},
{ deep: true }
)
watch(
list,
() => {
nextTick(() =>
console.log('list: ', list.value.length, items.value.length)
)
},
{ deep: true }
)
return { list, items }
}
})
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>
The original statement is correct: useTemplateRef
is not reactive for arrays, as of Vue 3.5, but this may change in a future version.
See this helpful discussion with a Vue team member about the topic: https://github.com/orgs/vuejs/discussions/12989
It seems like this was a design mistake that they intend to correct.
Not trying to be pedantic here, but @tao's answer is not quite right, and I want to clarify because it could confuse someone.
itemRefs is updated (e.g reactive). But it's always updated 1 render cycle after list has changed. Wrap your console.log in nextTick (imported from 'vue') and the numbers will match:
itemRefs
is not reactive. Indeed, itemRefs
will have the expected value after a nextTick
. But the "show code snippet" example works because you are watching the list
source (which is truly reactive) and it ultimately determines how many itemRefs
there will be. But if you were to just watch itemRefs
as the source, it would not work.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With