Tutorials Logic, IN info@tutorialslogic.com

Vue Lists Keys v for key Attribute

List Identity

The v-for directive renders one block for each item in an iterable. The :key value tells Vue which rendered block represents which data item across insertions, removals, sorting, and filtering. A stable key preserves component state and DOM identity; it is not merely a warning-suppression attribute.

This lesson assumes reactive arrays and template syntax. You will learn to choose a key, update lists without losing reactivity, and avoid filtered or sorted expressions that make rendering harder to reason about.

v-for - Rendering Lists

The v-for directive renders a list of items based on an array or object. Always provide a :key attribute with a unique, stable identifier - this helps Vue efficiently update the DOM when the list changes.

v-for - Arrays, Objects, Ranges, Filtering, Sorting

v-for - Arrays, Objects, Ranges, Filtering, Sorting
<template>
  <div>
    <!-- Array with index -->
    <ul>
      <li v-for="(fruit, index) in fruits" :key="fruit">
        {{ index + 1 }}. {{ fruit }}
      </li>
    </ul>

    <!-- Array of objects - use stable ID as key -->
    <div v-for="user in users" :key="user.id" class="user-card">
      <h3>{{ user.name }}</h3>
      <p>{{ user.email }}</p>
    </div>

    <!-- Object properties -->
    <dl>
      <template v-for="(value, key, index) in person" :key="key">
        <dt>{{ index + 1 }}. {{ key }}</dt>
        <dd>{{ value }}</dd>
      </template>
    </dl>

    <!-- Range (1 to n) -->
    <span v-for="n in 5" :key="n">{{ n }} </span>

    <!-- Nested v-for -->
    <div v-for="category in categories" :key="category.id">
      <h3>{{ category.name }}</h3>
      <ul>
        <li v-for="item in category.items" :key="item.id">
          {{ item.name }}
        </li>
      </ul>
    </div>

    <!-- Filtered list with computed -->
    <input v-model="search" placeholder="Search..." />
    <ul>
      <li v-for="user in filteredUsers" :key="user.id">
        {{ user.name }}
      </li>
    </ul>

    <!-- v-for + v-if - use <template> to avoid conflict -->
    <template v-for="user in users" :key="user.id">
      <div v-if="user.isActive">{{ user.name }}</div>
    </template>
  </div>
</template>

<script setup>
import { ref, reactive, computed } from 'vue'

const fruits = ref(['Apple', 'Banana', 'Cherry', 'Date'])
const users = reactive([
  { id: 1, name: 'Alice', email: 'alice@example.com', isActive: true },
  { id: 2, name: 'Bob',   email: 'bob@example.com',   isActive: false },
  { id: 3, name: 'Carol', email: 'carol@example.com', isActive: true },
])
const person = reactive({ name: 'Alice', age: 25, city: 'NYC' })
const categories = reactive([
  { id: 1, name: 'Fruits', items: [{ id: 11, name: 'Apple' }, { id: 12, name: 'Banana' }] },
  { id: 2, name: 'Veggies', items: [{ id: 21, name: 'Carrot' }] },
])
const search = ref('')

const filteredUsers = computed(() =>
  users.filter(u => u.name.toLowerCase().includes(search.value.toLowerCase()))
)
</script>

v-for - Rendering Lists - HTML Example

v-for - Rendering Lists - HTML Example
<template>
  <div>
    <div class="controls">
      <input v-model="newTask" @keyup.enter="addTask" placeholder="New task..." />
      <select v-model="sortBy">
        <option value="name">Sort by Name</option>
        <option value="priority">Sort by Priority</option>
      </select>
    </div>

    <TransitionGroup name="list" tag="ul">
      <li v-for="task in sortedTasks" :key="task.id" class="task-item">
        <input type="checkbox" v-model="task.done" />
        <span :class="{ done: task.done }">{{ task.name }}</span>
        <select v-model="task.priority">
          <option value="high">High</option>
          <option value="medium">Medium</option>
          <option value="low">Low</option>
        </select>
        <button @click="removeTask(task.id)">x</button>
      </li>
    </TransitionGroup>

    <p>{{ tasks.filter(t => t.done).length }} / {{ tasks.length }} done</p>
  </div>
</template>

<script setup>
import { ref, reactive, computed } from 'vue'

const newTask = ref('')
const sortBy = ref('name')
const tasks = reactive([
  { id: 1, name: 'Learn Vue', priority: 'high',   done: false },
  { id: 2, name: 'Build app', priority: 'medium', done: false },
  { id: 3, name: 'Deploy',    priority: 'low',    done: false },
])

const sortedTasks = computed(() => {
  return [...tasks].sort((a, b) => {
    if (sortBy.value === 'name') return a.name.localeCompare(b.name)
    const order = { high: 0, medium: 1, low: 2 }
    return order[a.priority] - order[b.priority]
  })
})

function addTask() {
  if (!newTask.value.trim()) return
  tasks.push({ id: Date.now(), name: newTask.value, priority: 'medium', done: false })
  newTask.value = ''
}

function removeTask(id) {
  const idx = tasks.findIndex(t => t.id === id)
  if (idx !== -1) tasks.splice(idx, 1)
}
</script>

<style>
.list-enter-active, .list-leave-active { transition: all 0.3s ease; }
.list-enter-from, .list-leave-to { opacity: 0; transform: translateX(-20px); }
.list-move { transition: transform 0.3s ease; }
.done { text-decoration: line-through; opacity: 0.5; }
</style>

Stable Keys

Use a primitive identifier that belongs to the record, such as product.id. Do not use the array index when order can change: after sorting or inserting, the same index may represent a different record while Vue reuses the old DOM or component instance. That mismatch can move input values, focus, or local component state to the wrong row.

Keys must be unique among siblings and stable for the lifetime of the item. Math.random() creates a new identity on every render and forces replacement. An object is also unsuitable because key comparison needs a primitive value with predictable equality.

Derived Lists

Keep the original collection as source data and derive visible rows with a computed property. A computed filter or sort is cached by reactive dependencies and gives the template a clean list to render. Avoid calling mutating sort() directly on shared source data unless changing the source order is the intended action; use a copied array for a display-only order.

Vue tracks array mutation methods and assignments to a ref. When replacing a ref-held array, assign the new array to items.value. When removing records, base the operation on the stable identifier rather than the current screen index, especially if the screen is filtered.

List Diagnosis

When rows appear to display another row’s input, inspect the key first. Then verify that the key does not change when a label changes and that duplicate IDs are not entering the data. The Vue warning points to a rendering symptom; the durable fix is to repair data identity.

Before you move on

Identity Review

4 checks
  • Use a unique primitive key from the record.
  • Avoid index keys for sortable, filterable, or editable lists.
  • Derive visible lists with computed state.
  • Update or remove records by identifier.

Row Mismatches

  • Using the array index as a changing row’s key.

    Use a stable record ID.
  • Generating a random key during render.

    Create identity when the record is created and retain it.
  • Sorting source state for display.

    Sort a copy in a computed property unless the source order should change.

Try this next

Prove Row Identity

0 of 2 completed

Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.