In Vue 3, what does a template ref placed on an element inside `v-for` hold, and why must you not rely on its order?
answer
- one key, many elements
- an array, not one element
- filled after mount
- not the source order
- map by id when order matters
basics
~20 sA ref inside v-for collects an array of all rendered elements or component instances. Vue does not guarantee that the array matches the source list's order, so look items up by an identifier, not by index.
solid answer
~40 sWhen `ref="rows"` sits on an element rendered by `v-for`, Vue fills the ref with an **array** of the elements (or component instances), whether you use `useTemplateRef('rows')` in 3.5 or a same-named `ref([])`. The array is populated after mount, grows as items render and shrinks when they unmount. The Vue docs state it does **not** guarantee the source array's order: entries are appended as elements are set, so after inserts, removals or reordering, `rows.value[i]` may not be the element for `list[i]`. When you need the element for a specific item, use a function ref that stores elements in a `Map` keyed by the item's id, or read a `data-id` attribute from the element.
code
vue · 18 lines<script setup>
import { ref, useTemplateRef } from 'vue'
const users = ref([{ id: 1, name: 'Ann' }, { id: 2, name: 'Bo' }])
const rowEls = useTemplateRef('rows')
function scrollToUser(id) {
// wrong: rowEls.value[index] may not match users.value[index]
const el = rowEls.value?.find((r) => r.dataset.id === String(id))
el?.scrollIntoView()
}
</script>
<template>
<ul>
<li v-for="u in users" :key="u.id" :data-id="u.id" ref="rows">{{ u.name }}</li>
</ul>
</template>go deeper
Remember that a ref inside v-for gives you an array of elements after mount.
Explain how the runtime appends and removes entries, and why that breaks index correspondence with the list.
Catch index-based lookups in review and replace them with an id-keyed map from a function ref.
Decide whether list components should expose per-item element access at all, or keep imperative access behind a small API.
## One ref key, many elements A template ref normally points at one element. When the element carrying `ref` is rendered by `v-for`, there are many elements for one key. Vue's compiler marks such refs as **inside a loop**, and the runtime collects every element into an **array** instead of overwriting a single value. ```vue <script setup> import { ref, useTemplateRef, onMounted } from 'vue' const users = ref([{ id: 1, name: 'Ann' }, { id: 2, name: 'Bo' }]) const rowEls = useTemplateRef('rows') onMounted(() => console.log(rowEls.value.length)) // 2 </script> <template> <li v-for="u in users" :key="u.id" ref="rows">{{ u.name }}</li> </template> ``` The same applies with the pre-3.5 pattern `const rows = ref([])` plus `ref="rows"`, and to components rendered in a loop, where the array holds component instances. ## How the array is maintained The runtime keeps one array per ref key: - On first mount, each rendered element is **pushed** into the array after rendering. - When a new item renders later, its element is appended if not already present. - When an item's element unmounts, that element is **removed** from the array. - The array is not rebuilt from the list on every update. That last point explains the ordering warning. ## Why the order is not guaranteed The Vue guide says plainly that the ref array does **not** guarantee the same order as the source array. Because entries are appended as elements are set and removed individually, the array reflects **when** elements were mounted, not **where** they are in the list: 1. Render `[A, B, C]`: the array is `[elA, elB, elC]`. 2. Insert `X` at the front of the list: the array becomes `[elA, elB, elC, elX]`. 3. Now `rows.value[0]` is A's element, while `users.value[0]` is X. Code such as `rows.value[index].scrollIntoView()` then scrolls to the wrong row. It usually works in a demo with a static list and fails once the list is sorted, filtered or prepended in production. | Need | Reliable approach | |---|---| | Count or iterate all rendered rows | the ref array is fine | | Element for a specific item | a `Map` filled by a function ref, keyed by id | | Element for the visible position | query the container's children in DOM order | | Element for one item only | put a separate ref on that one element | ## Mapping elements to items reliably A **function ref** receives each element and lets you store it wherever you like: ```vue <script setup> const rowById = new Map() function setRow(id, el) { if (el) rowById.set(id, el) else rowById.delete(id) } </script> <template> <li v-for="u in users" :key="u.id" :ref="(el) => setRow(u.id, el)"> {{ u.name }} </li> </template> ``` The element arrives on mount and on updates, and `null` arrives when the element unmounts, so the map stays accurate. `rowById.get(user.id)` then returns the right row regardless of order. Alternatively, read `el.dataset.id` from elements in the ref array. ## Other details - The array is empty (or `null` for `useTemplateRef` before mount) until the first render completes, so read it in `onMounted` or later. - After you change the list, the array updates only after Vue re-renders; wait for the DOM update before reading it. - A `:key` on the `v-for` is still needed for correct patching; it does not make the ref array ordered. ## Interview summary Say three things: it is an array, it is filled after mount and kept in sync as items come and go, and its order is not the list's order, so map by id when you need a specific item. ## Components rendered in the loop When the `ref` sits on a component inside `v-for`, the array holds **component instances**, maintained by the same append-and-remove rules. For `<script setup>` children, each entry only offers what that child passed to `defineExpose`, so iterating `cards.value.forEach((c) => c.reset())` works only if every card exposes `reset`. Because the entries are unordered, such broadcast calls are fine, while "reset the third card" should again go through an id-keyed map. When a card unmounts, its instance leaves the array, so holding on to an entry after filtering the list can keep a stale instance alive in your own variables.
- In Vue 3, does adding `:key` to the `v-for` make the template ref array follow the list order?No. The key controls how Vue matches and moves list elements during patching, but the ref array is maintained by appending and removing entries as elements mount and unmount. Its order reflects mount history, so you still map by id or query DOM order when position matters.
- What does a Vue 3 `ref` inside `v-for` hold when the loop renders child components instead of elements?An array of the child component instances, with the same rules: filled after mount, updated as items come and go, no ordering guarantee. For `<script setup>` children, each entry only exposes what the child passed to `defineExpose`.
saying these in an interview costs you the question
- Expects a ref inside v-for to hold only the last element
- Assumes rows.value[i] always matches list[i]
- Believes adding :key makes the ref array ordered
- Reads the ref array in setup before anything is rendered
- Thinks each loop iteration needs its own useTemplateRef call