Chapter 73: Vue v-for Directive
V-for
This is the directive you use to loop over arrays or objects and render a block of template for each item — exactly like a for…of loop in JavaScript, but declaratively inside HTML.
Without v-for, you would have to write the same <li>, <div class=”card”>, <tr>, or <option> tag 10, 20, 100 times manually. With v-for, you write it once and let Vue repeat it for you — and it stays reactive: when the array changes (add, remove, reorder), Vue intelligently updates only the necessary parts of the DOM.
1. Basic Syntax – The Golden Rule You Must Never Break
|
0 1 2 3 4 5 6 7 8 |
<div v-for="item in items" :key="item.id"> <!-- content using item --> </div> |
The #1 unbreakable rule in Vue 3 (2026)
You must always provide a :key attribute on the element that has v-for.
- :key must be unique within the current list
- :key must be stable — the same item must always have the same key value
- :key should be a primitive (string or number) — never an object
2. Why :key is mandatory – The bugs you will see without it
Bug 1 – Input focus jumps / lost when deleting items
|
0 1 2 3 4 5 6 7 8 9 |
<!-- WRONG – no :key --> <li v-for="todo in todos"> <input type="text" v-model="todo.text" /> </li> |
→ Type in the second input → delete the first item → focus jumps to the wrong input or disappears → Reason: Vue re-uses DOM elements based on position → the second <input> becomes the first one
Bug 2 – Animations break or look wrong
When using <TransitionGroup> without :key → items don’t animate correctly on reorder/delete
Bug 3 – State of child components gets mixed up
|
0 1 2 3 4 5 6 |
<ChildComponent v-for="item in items" :item="item" /> |
→ Without :key, when list order changes → child components are reused in wrong order → internal state (checkboxes, form values) jumps to wrong item
Correct version – always with :key
|
0 1 2 3 4 5 6 7 8 |
<li v-for="todo in todos" :key="todo.id"> <input type="text" v-model="todo.text" /> </li> |
→ Delete first item → focus stays on the correct input → Reorder → child state stays with the correct item
3. Real, Practical Example – Todo List with Filtering, Sorting & Animations
|
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 |
<template> <div class="todo-app"> <h1>Todo List with v-for</h1> <div class="controls"> <input v-model.trim="newTask" placeholder="Add new task..." @keyup.enter="addTask" /> <button @click="addTask">Add</button> <select v-model="sortBy"> <option value="none">No sort</option> <option value="text">Sort by text</option> <option value="done">Sort by status</option> </select> <select v-model="filter"> <option value="all">All</option> <option value="active">Active</option> <option value="completed">Completed</option> </select> </div> <!-- The star: TransitionGroup + correct :key --> <TransitionGroup name="list" tag="ul"> <li v-for="todo in sortedAndFilteredTodos" :key="todo.id" <!-- ★ correct stable key ★ --> class="todo-item" > <input type="checkbox" :checked="todo.done" @change="toggleDone(todo.id)" /> <span :class="{ done: todo.done }">{{ todo.text }}</span> <button @click="removeTodo(todo.id)">×</button> </li> </TransitionGroup> <p v-if="!sortedAndFilteredTodos.length" class="empty"> No tasks yet — add one! </p> <p class="stats">{{ remaining }} remaining</p> </div> </template> <script setup lang="ts"> import { ref, computed } from 'vue' interface Todo { id: number text: string done: boolean } const newTask = ref('') const todos = ref<Todo[]>([ { id: 1, text: 'Learn v-for properly', done: true }, { id: 2, text: 'Always use :key', done: false }, { id: 3, text: 'Master animations', done: false } ]) const sortBy = ref<'none' | 'text' | 'done'>('none') const filter = ref<'all' | 'active' | 'completed'>('all') const sortedAndFilteredTodos = computed(() => { let list = [...todos.value] // Filter if (filter.value === 'active') list = list.filter(t => !t.done) if (filter.value === 'completed') list = list.filter(t => t.done) // Sort if (sortBy.value === 'text') { list.sort((a, b) => a.text.localeCompare(b.text)) } else if (sortBy.value === 'done') { list.sort((a, b) => Number(a.done) - Number(b.done)) } return list }) const remaining = computed(() => todos.value.filter(t => !t.done).length) function addTask() { if (!newTask.value.trim()) return todos.value.push({ id: Date.now(), text: newTask.value.trim(), done: false }) newTask.value = '' } function toggleDone(id: number) { const todo = todos.value.find(t => t.id === id) if (todo) todo.done = !todo.done } function removeTodo(id: number) { todos.value = todos.value.filter(t => t.id !== id) } </script> <style scoped> /* ... previous styles ... */ /* TransitionGroup classes */ .list-enter-active, .list-leave-active, .list-move { transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); } .list-enter-from, .list-leave-to { opacity: 0; transform: translateX(-40px) scale(0.95); } .list-leave-active { position: absolute; width: 100%; } </style> |
Quick Summary Table – v-for Essentials (2026)
| Feature | Syntax / Rule | Why it’s critical |
|---|---|---|
| Basic loop | v-for=”item in items” | — |
| With index | v-for=”(item, index) in items” | Useful for numbering |
| With key (MANDATORY) | :key=”item.id” | Reactivity, animations, performance |
| Loop over object | v-for=”(value, key) in object” | Rendering object properties |
| Use <template> for no wrapper | <template v-for=”…”> … </template> | Clean DOM |
| Inside <TransitionGroup> | :key + tag=”ul” or tag=”div” | Animations work |
| Never use index as key | :key=”index” → only for static lists | Breaks on reorder/delete |
Pro Tips from Real Projects (Hyderabad 2026)
- Always use :key — Vue shows console warning if missing
- Prefer database ID / UUID for :key — most stable
- For local-only lists → Date.now() or incremental counter is fine
- Use <template v-for> when you don’t want extra wrapper element
- For animations → always pair with <TransitionGroup> + :key
- Never mutate array with methods that break reactivity (push is fine, but items.length = 0 breaks → use items.value = [])
Your mini homework:
- Build the todo list above
- Add sorting & filtering → watch how smoothly items move with correct :key
- Remove :key → see broken animations & lost focus
- Add <template v-for> for a group of elements without wrapper
Any part confusing? Want full examples for:
- v-for + <TransitionGroup> + drag-and-drop?
- v-for on object + index/key/value?
- v-for inside <select> for options?
- Common bug with :key=”index” + inputs?
Just tell me — we’ll build the next perfect list together 🚀
