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>
Rule of thumb

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, because 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.