Fundamentals
Lists and why the key matters
You have a list and you want one element per item. v-for does that, and
:key is what separates a list that behaves from a list that loses the focus of a
field every time someone filters it.
The basics
<div v-data="{ products: [
{ id: 1, name: 'Mug', price: 39 },
{ id: 2, name: 'Shirt', price: 89 }
] }">
<ul>
<li v-for="p in products" :key="p.id">{ p.name } — $ { p.price }</li>
</ul>
<button @click="products.push({ id: Date.now(), name: 'New', price: 10 })">
Add
</button>
</div>
Every accepted form
v-for is not limited to arrays. in and of are equivalent.
<div v-data="{
users: [{ id: 1, name: 'Ann' }, { id: 2, name: 'Bob' }],
settings: { theme: 'dark', language: 'en-US' }
}">
<p>With an index:</p>
<ul><li v-for="(user, i) in users" :key="user.id">{ i }: { user.name }</li></ul>
<p>Over an object:</p>
<ul><li v-for="(value, key) in settings">{ key } = { value }</li></ul>
<p>Over a number:</p>
<ul><li v-for="n in 3">Item { n }</li></ul>
<p>Over a string:</p>
<ul><li v-for="letter in 'abc'">{ letter }</li></ul>
</div>
Arrays, numbers, strings, objects, Map and Set all work. Over a number,
counting starts at 1 — v-for="n in 3" gives 1, 2 and 3.
To repeat a group of elements with no wrapper, <template> works here too:
<dl>
<template v-for="p in products" :key="p.id">
<dt>{ p.name }</dt>
<dd>{ p.price }</dd>
</template>
</dl>
Why the key matters
:key tells Voodoo how to recognize the same item across two updates. With no key,
the library only knows the position: if the list is reordered or filtered, it reuses the element
at position 1 for whatever the new item at position 1 happens to be.
In practice that means fields losing focus, text being edited disappearing and the scroll jumping. Compare the two lists below: type into one of the fields and then click to shuffle.
<div v-data="{ tasks: [
{ id: 1, text: 'Buy coffee' },
{ id: 2, text: 'Answer email' },
{ id: 3, text: 'Pay the bill' }
] }">
<button @click="tasks.reverse()">Reverse the order</button>
<p><b>With :key</b></p>
<div v-for="t in tasks" :key="t.id"><input v-model="t.text"></div>
<p><b>Without :key</b></p>
<div v-for="t in tasks"><input v-model="t.text"></div>
</div>
Whenever the list can be reordered, filtered or have items removed from the middle, put a
:key on it. The key has to be stable and unique within the list: the
id from the database works; the position index does not — it changes along with
the order, which is exactly the problem the key was supposed to solve.
Filtering and sorting
There is no filter built into the directive: the v-for expression accepts any
expression that returns a collection, so filter, slice and
sort handle it — with the caveat that sort mutates the original array,
so it is worth copying first.
<div v-data="{
search: '',
products: [
{ id: 1, name: 'Mug', price: 39 },
{ id: 2, name: 'Shirt', price: 89 },
{ id: 3, name: 'Notebook', price: 25 }
]
}">
<input v-model="search" placeholder="Filter...">
<ul>
<li v-for="p in products.filter(p => p.name.toLowerCase().includes(search.toLowerCase()))"
:key="p.id">
{ p.name } — $ { p.price }
</li>
</ul>
<p v-if="!products.some(p => p.name.toLowerCase().includes(search.toLowerCase()))">
Nothing found.
</p>
</div>
When the expression gets too long for the attribute, move it into a function on the
v-data — the result is the same and the HTML is readable again.
v-for and v-if do not share an element
Both take control of that node, so the library has no way of deciding which one wins. Put the
condition on a child, or wrap the repetition in a <template>.
<!-- do not do this -->
<li v-for="n in list" v-if="n % 2 === 0">{ n }</li>
<!-- do this -->
<li v-for="n in list">
<span v-if="n % 2 === 0">{ n }</span>
</li>
<!-- or filter in the expression itself -->
<li v-for="n in list.filter(n => n % 2 === 0)">{ n }</li>
What exists inside each item
Each iteration creates a scope of its own. Inside it you have the item variable, the index when
declared, and everything the outer scopes offer. The magic variables stay available, with
$el pointing at the element of that iteration.
<div v-data="{ label: 'Product', items: ['Mug', 'Shirt'] }">
<ul>
<li v-for="(item, i) in items">
{ label } { i + 1 }: { item }
<button @click="items.splice(i, 1)">remove</button>
</li>
</ul>
<p v-if="!items.length">The list is empty.</p>
</div>
The limit worth knowing about
v-for renders every item in the source: there is no virtualization. For lists of a
few hundred rows that is irrelevant. For tens of thousands updating at once, paginate, use
infinite scrolling with v-infinite-scroll, or pick a tool that virtualizes. The
performance page measures the real cost with
numbers.