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 — 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.