v-for
v-for renders a template once per item in an array, object, or numeric range. Always bind a :key — it’s how Vue tracks which DOM nodes to reuse vs recreate.
Lists, objects, ranges, key strategies
EXAMPLE
<script setup>
import { ref, computed } from 'vue';
const todos = ref([
{ id: 1, title: 'Ship feature', done: false },
{ id: 2, title: 'Write tests', done: true },
{ id: 3, title: 'Update docs', done: false },
]);
const filter = ref('all');
const filtered = computed(() => {
if (filter.value === 'done') return todos.value.filter(t => t.done);
if (filter.value === 'todo') return todos.value.filter(t => !t.done);
return todos.value;
});
</script>
<template>
<!-- 1) Loop an array -->
<ul>
<li v-for="todo in filtered" :key="todo.id">
<input type="checkbox" v-model="todo.done" />
<span :class="{ done: todo.done }">{{ todo.title }}</span>
</li>
</ul>
<!-- 2) With index — (item, index) shape -->
<ol>
<li v-for="(todo, i) in filtered" :key="todo.id">
{{ i + 1 }}. {{ todo.title }}
</li>
</ol>
<!-- 3) Object iteration — (value, key, index) -->
<dl>
<template v-for="(value, key) in headers" :key="key">
<dt>{{ key }}</dt>
<dd>{{ value }}</dd>
</template>
</dl>
<!-- 4) Numeric range — 1..N -->
<span v-for="n in 5" :key="n" class="star">★</span>
<!-- 5) Nested loop — composite key -->
<ul v-for="section in sections" :key="section.id">
<h3>{{ section.title }}</h3>
<li v-for="item in section.items" :key="\`${section.id}-${item.id}\`">
{{ item.label }}
</li>
</ul>
<!-- 6) v-for on a <template> (no extra wrapper element) -->
<template v-for="row in rows" :key="row.id">
<tr>
<td>{{ row.name }}</td>
<td>{{ row.value }}</td>
</tr>
</template>
<!-- 7) v-for + v-if — Vue 3: use a computed or wrap with template -->
<!-- BAD: v-if on the same element as v-for is no longer allowed -->
<template v-for="todo in todos" :key="todo.id">
<li v-if="!todo.done">{{ todo.title }}</li>
</template>
<!-- Or pre-filter with a computed (preferred) -->
<!-- 8) Reuse a Component in the list -->
<TodoItem v-for="t in todos" :key="t.id" :todo="t" @done="markDone" />
</template>
<style scoped>
.done { text-decoration: line-through; opacity: 0.6; }
</style>
<!-- 9) Performance + key strategy -->
<!-- The key MUST be stable + unique across the list (NOT the index).
Using the index causes Vue to reuse DOM nodes for the wrong items
when the list is reordered, filtered, or items are inserted in the middle. -->
<!-- ❌ -->
<li v-for="(t, i) in todos" :key="i">…</li>
<!-- ✅ -->
<li v-for="t in todos" :key="t.id">…</li>
<!-- 10) Reactivity caveats — mutating arrays
These trigger updates (wrapped by Vue's reactivity):
push / pop / shift / unshift / splice / sort / reverse
These DON'T (you must reassign):
array[i] = newValue → use array.splice(i, 1, newValue)
array.length = N → use array.splice(N) -->
Why it matters
The :key is what lets Vue identify a node across renders. Use a stable, unique business key (id); never use the array index unless the list is genuinely static.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Exercise
Loop over items.
<li v-for="item in items"
="item.id">
Bound special attribute.
Discussion
Loading…