Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

useTemplateRef is not reactive for arrays?

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:

shallow ref

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>
like image 756
winwin Avatar asked Aug 31 '25 20:08

winwin


2 Answers

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>
like image 95
tao Avatar answered Sep 04 '25 08:09

tao


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.

like image 25
V. Rubinetti Avatar answered Sep 04 '25 08:09

V. Rubinetti