List Rendering with v-for
v-for is Vue's directive for rendering a list of elements based on an array, object, number range, or iterable. It uses the syntax item in items, and Vue creates one copy of the element (and its subtree) per entry in the source. Under the hood, v-for is closely tied to Vue's diffing algorithm: whenever the source data changes, Vue needs an efficient way to figure out which DOM nodes to keep, move, add, or remove, and that efficiency hinges almost entirely on how you supply the key attribute.
Cricket analogy: v-for is like a scorer writing one line per batsman in the order they come in — but when the batting order changes mid-innings, the scorer needs each player's fixed cap number (the key), not their position in the list, to correctly track who's who.
Basic syntax and destructuring
The simplest form is v-for="item in items", which exposes item for each iteration. You can also capture the index with v-for="(item, index) in items". For objects, v-for supports (value, key, index) in object, iterating over the object's own enumerable properties. v-for also works on a plain number with v-for="n in 10", producing values 1 through 10 (not 0 through 9), which is convenient for generating a fixed count of placeholder elements. Just like v-if, v-for can be applied to a <template> tag when you need to render a group of sibling elements per iteration without introducing an extra wrapper element.
Cricket analogy: v-for="n in 10" is like a scorecard app generating ten empty over-boxes before a match starts (overs 1 through 10, not 0 through 9); v-for="(player, index)" additionally tracks each player's batting position, and wrapping a <template> lets a scorer group a bowler's figures without an extra wrapper row.
Why the key attribute matters
Vue's virtual DOM patching algorithm, by default, tries to reuse and patch existing elements in place rather than recreating them, which is normally a performance win. For lists, Vue needs a stable identity per item to know which existing DOM node corresponds to which data entry after a list changes — that identity is the key. Without a unique, stable key (or worse, using the array index as the key on a list that gets reordered, filtered, or has items inserted in the middle), Vue may reuse the wrong DOM node for the wrong data, causing bugs where an input's typed value, a component's local state, or a CSS transition ends up attached to the wrong list item after the list changes. The key must be a primitive (string or number) that uniquely and stably identifies each item — typically a database id, never the loop index for dynamic lists.
Cricket analogy: Using a batsman's position in the order as the key backfires when a batsman retires hurt and is replaced mid-list — Vue might reuse the wrong scorecard row, attaching one player's "not out" status to a completely different player's name; the fix is keying by each player's fixed cap number.
<script setup>
import { ref } from 'vue'
const tasks = ref([
{ id: 101, title: 'Write proposal', done: false },
{ id: 102, title: 'Review PR', done: true },
{ id: 103, title: 'Deploy staging', done: false }
])
function removeTask(id) {
tasks.value = tasks.value.filter((task) => task.id !== id)
}
</script>
<template>
<ul>
<li v-for="task in tasks" :key="task.id">
<input type="checkbox" v-model="task.done" />
<span :class="{ done: task.done }">{{ task.title }}</span>
<button @click="removeTask(task.id)">Remove</button>
</li>
</ul>
</template>This is the same problem React solves with its key prop on mapped JSX elements — both frameworks use a keyed reconciliation strategy, and both explicitly warn against using the array index as a key when the list can be reordered or filtered. The underlying reason is identical: index-based keys are stable only as long as items never move, which defeats the purpose of a stable identity.
Never combine v-for and v-if on the same element in Vue 3 — as of Vue 3.x, when both are present on one element, v-if is evaluated first per Vue's compiler behavior, meaning it can't access variables from the v-for scope, and this combination is explicitly flagged by the recommended ESLint rules. Instead, filter the source array with a computed property before looping, or move the v-if to a wrapping <template> tag.
- v-for iterates arrays (
item in items), objects ((value, key) in obj), and number ranges (n in 10, 1-indexed). - A stable, unique
key(never the raw index for dynamic lists) is required so Vue's patch algorithm can correctly match DOM nodes to data across updates. - Missing or unstable keys can cause input values, component state, or transitions to attach to the wrong list item after reordering or filtering.
- v-for can be applied to a
<template>tag to group multiple sibling elements per iteration. - v-for and v-if should never sit on the same element; filter the source list with a computed property instead.
- v-for supports destructuring the item and capturing the index in the same expression:
(item, index) in items.
Practice what you learned
1. What is the primary purpose of the `key` attribute in a v-for loop?
2. What value does `v-for="n in 5"` produce?
3. Why is using the array index as the key generally discouraged for dynamic lists?
4. What is the recommended way to render only a filtered subset of a v-for list in Vue 3?
5. Which syntax correctly captures both the object property key and its index while iterating an object with v-for?
Was this page helpful?
You May Also Like
Conditional Rendering: v-if and v-show
Explains Vue's two conditional rendering directives, how they differ at the DOM level, and how to choose the right one for performance and correctness.
reactive() and ref()
A deep dive into Vue 3's two core reactivity primitives — ref() for any value type and reactive() for objects — and when to use each.
Template Syntax
A tour of Vue's HTML-based template syntax, including text interpolation, directive bindings, and the shorthand notations for v-bind and v-on.
Computed Caching and Performance
Explains how Vue's computed properties memoize their results based on reactive dependencies, and how to use that caching behavior deliberately to avoid wasteful recalculation in components.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebSockets Study Notes
Web Development · 30 topics
Web DevelopmentWebAssembly Study Notes
WebAssembly · 30 topics
Web DevelopmentgRPC Study Notes
Protocol Buffers · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentFlask Study Notes
Python · 30 topics
Web DevelopmentDjango Study Notes
Python · 30 topics