List Rendering with v-for
Rendering arrays and objects with v-for, and why the :key attribute isn't optional in practice.
3 min de lectura
v-for renders an element once per item in an array (or object, or range), using syntax borrowed from JavaScript's for...of:
<script setup>
import { ref } from "vue";
const fruits = ref(["Apple", "Banana", "Cherry"]);
</script>
<template>
<ul>
<li v-for="fruit in fruits" :key="fruit">{{ fruit }}</li>
</ul>
</template>You can also access the index as a second variable:
<template>
<li v-for="(fruit, index) in fruits" :key="fruit">{{ index }}: {{ fruit }}</li>
</template>Why :key is not optional
Every v-for in this lesson includes a :key, bound to something unique and stable per item — and skipping it is one of the most common sources of subtle bugs in real Vue apps.
Without a key, Vue's default strategy for updating a list after a change is to patch elements in place by position, reusing whatever DOM node already happens to sit at each index. If you insert an item at the start of a list without keys, Vue doesn't realize a new item was inserted — it just sees that the content at every index shifted, and patches each existing DOM node's content to match, rather than creating one new node and leaving the rest alone. For plain text that's merely wasteful. For a list of inputs holding typed text, or components holding their own internal state, it actively puts the wrong content next to the wrong item.
<script setup>
import { ref } from "vue";
const todos = ref([
{ id: 1, text: "Buy milk" },
{ id: 2, text: "Walk the dog" },
]);
</script>
<template>
<!-- Good: keyed by a stable, unique id -->
<li v-for="todo in todos" :key="todo.id">{{ todo.text }}</li>
</template>Use a genuinely stable identifier — a database ID, not the array index — as the key. Using the index as a key defeats the purpose for the same reason as having no key at all: the index changes whenever an item is inserted or removed anywhere before it.
Looping over objects
<script setup>
import { reactive } from "vue";
const user = reactive({ name: "Ada", role: "Engineer", location: "London" });
</script>
<template>
<ul>
<li v-for="(value, key) in user" :key="key">{{ key }}: {{ value }}</li>
</ul>
</template>The order of arguments is (value, key), not (key, value) — easy to get backwards coming from Object.entries().
Filtering: use a computed property, not v-if on the same element
A common instinct is reaching for v-if right next to v-for to filter a list:
<!-- Avoid: v-if and v-for on the same element -->
<li v-for="todo in todos" v-if="!todo.done" :key="todo.id">{{ todo.text }}</li>Beyond the precedence pitfalls mentioned in the previous lesson, this also re-evaluates the filter condition on every single re-render for every item. Filtering with a computed() first, then looping over the result, is both clearer and only recomputes when todos actually changes:
<script setup>
import { ref, computed } from "vue";
const todos = ref([
{ id: 1, text: "Buy milk", done: false },
{ id: 2, text: "Walk the dog", done: true },
]);
const activeTodos = computed(() => todos.value.filter((t) => !t.done));
</script>
<template>
<li v-for="todo in activeTodos" :key="todo.id">{{ todo.text }}</li>
</template>