Chapter 7: Vue v-for
Think of v-for as Vue’s built-in forEach that magically turns your data array (or object) into repeated HTML elements — and keeps them in sync when the data changes (add/remove/reorder items).
Official Vue 3 docs (vuejs.org/guide/essentials/list.html) say:
We can use the v-for directive to render a list of items based on an array. The v-for directive requires a special syntax in the form of item in items, where items is the source data array and item is an alias for the array element being iterated on.
Let’s break it down step by step with real, runnable examples.
1. Basic Syntax – Arrays (most common case)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
<template> <ul class="fruits-list"> <li v-for="fruit in fruits" :key="fruit"> {{ fruit }} </li> </ul> </template> <script setup> import { ref } from 'vue' const fruits = ref(['Mango', 'Guava', 'Sapota', 'Custard Apple']) </script> <style scoped> .fruits-list { list-style: none; padding: 0; } .fruits-list li { padding: 0.8rem 1rem; margin: 0.5rem 0; background: #fef3c7; border-radius: 6px; font-size: 1.1rem; } </style> |
→ Vue creates one <li> for each item → Change fruits.value.push(‘Jackfruit’) → new <li> appears instantly
Important rule #1 (never forget this!) Always add :key attribute with a unique value per item.
Why? Vue uses the :key to track identity of each element when the list changes (add/delete/reorder). Without it → bugs like wrong animations, lost input focus, inefficient updates.
Bad (don’t do this in 2026):
|
0 1 2 3 4 5 6 |
<li v-for="fruit in fruits">{{ fruit }}</li> |
Good (always):
|
0 1 2 3 4 5 6 |
<li v-for="fruit in fruits" :key="fruit">{{ fruit }}</li> |
2. With Index – When you need position
|
0 1 2 3 4 5 6 7 8 9 10 |
<ol> <li v-for="(fruit, index) in fruits" :key="index"> {{ index + 1 }}. {{ fruit }} </li> </ol> |
Or more useful:
|
0 1 2 3 4 5 6 7 8 |
<div v-for="(user, idx) in team" :key="user.id"> <strong>#{{ idx + 1 }}</strong> {{ user.name }} ({{ user.role }}) </div> |
Note: Use :key=”user.id” (unique ID) instead of index whenever possible — index as key is only okay for static lists that never reorder/add/delete.
3. Looping over Objects
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<template> <dl class="person-info"> <template v-for="(value, key, index) in person" :key="key"> <dt>{{ key }}</dt> <dd>{{ value }}</dd> </template> </dl> </template> <script setup> import { reactive } from 'vue' const person = reactive({ name: 'Rahul Sharma', city: 'Hyderabad', age: 28, role: 'Frontend Lead', favoriteFramework: 'Vue 3' }) </script> |
→ You get three arguments: value, key, index → Use <template v-for> so no extra <div> wrapper
4. v-for with Components (very common in real apps)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
<!-- Parent: ProductGrid.vue --> <template> <div class="grid"> <ProductCard v-for="product in products" :key="product.id" :product="product" @add-to-cart="addToCart" /> </div> </template> <script setup> import ProductCard from '@/components/ProductCard.vue' // ... products ref array with { id, name, price, image } </script> <style scoped> .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1.5rem; } </style> |
Child (ProductCard.vue) receives prop and works normally.
5. v-for + v-if / v-else (careful combination!)
Never put v-if and v-for on the same element — Vue 3 evaluates v-if first → can cause bugs or unnecessary renders.
Wrong (avoid):
|
0 1 2 3 4 5 6 7 8 |
<li v-for="todo in todos" v-if="todo.done" :key="todo.id"> {{ todo.text }} </li> |
Correct ways:
A. Use computed (best performance)
|
0 1 2 3 4 5 6 |
const doneTodos = computed(() => todos.value.filter(t => t.done)) |
|
0 1 2 3 4 5 6 |
<li v-for="todo in doneTodos" :key="todo.id">{{ todo.text }}</li> |
B. Wrap with <template>
|
0 1 2 3 4 5 6 7 8 |
<template v-for="todo in todos" :key="todo.id"> <li v-if="todo.done">{{ todo.text }}</li> </template> |
6. Updating Lists Reactively – Best Practices 2026
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Good ways to mutate arrays (Vue tracks them) todos.value.push(newTodo) // add todos.value.splice(index, 1) // remove todos.value[index].done = true // update property todos.value = [...todos.value].sort(...) // reorder (new array) // Avoid (breaks reactivity or inefficient) todos.value = todos.value.filter(...) // ok but better to use computed todos.value.length = 0 // bad – use splice(0) |
Quick Cheat Sheet Table
| Syntax | Use Case | Must-have | Notes / 2026 Tip |
|---|---|---|---|
| v-for=”item in items” | Simple array | :key=”item.id” | Use unique ID |
| v-for=”(item, index) in items” | Need position / numbering | :key (prefer ID) | Index as key only static lists |
| v-for=”(val, key) in object” | Loop object properties | :key=”key” | Use <template> wrapper |
| v-for=”n in 10″ | Repeat N times (rare) | :key=”n” | Like range(1,11) |
| v-for + component | List of cards/users/posts | :key + props | Most common real usage |
| v-for + v-if on same tag | — | — | Never — use computed or <template> |
Pro Tips from Real 2026 Projects
- Always :key with unique stable value (database ID > UUID > index)
- Prefer computed for filtering/sorting → keeps template clean
- For very long lists (1000+ items) → look into v-memo (Vue 3.2+) or virtual scrolling libraries (vue-virtual-scroller, TanStack Virtual)
- Use <TransitionGroup> + v-for for animated list adds/removals
|
0 1 2 3 4 5 6 7 8 9 10 |
<TransitionGroup name="list" tag="ul"> <li v-for="item in items" :key="item.id"> {{ item.text }} </li> </TransitionGroup> |
Practice challenge: Build a dynamic todo list with:
- Add todo (push)
- Toggle done
- Delete (splice)
- Filter “active” / “completed” with computed + v-for
Any part confusing? Want full todo app code? Difference between in vs of? (both work, in is official) Or how to handle pagination / infinite scroll with v-for?
Just say — we’ll build it together step by step 🚀
Happy looping from Hyderabad! 🌶️💚
