VueJS #4 Rendering Lists 1.1

Catalogue
  1. 1. Maintaining State with key
  2. 2. v-for with a Component
    1. 2.1. Example of a Simple Todo List
  3. 3. Array Change Detection
    1. 3.1. Mutation Methods
    2. 3.2. Replacing an Array
  4. 4. Displaying Filtered/Sorted Results

Maintaining State with key

​When Vue is updating a list of elements rendered with v-for, by default it uses an “in-place patch” strategy. If the order of the data items has changed, instead of moving the DOM elements to match the order of the items, Vue will patch each element in-place and make sure it reflects what should be rendered at that particular index.

This default mode is efficient, but only suitable when your list render output does not rely on child component state or temporary DOM state (e.g. form input values).

To give Vue a hint so that it can track each node’s identity, and thus reuse and reorder existing elements, you need to provide a unique key attribute for each item:

1
2
3
<div v-for="item in items" :key="item.id">
  <!-- content -->
</div>

When using <template v-for>, the key should be placed on the <template> container:

1
2
3
<template v-for="todo in todos" :key="todo.name">
  <li>{{ todo.name }}</li>
</template>

Note

key here is a special attribute being bound with v-bind. It should not be confused with the property key variable when using v-for with an object.

It is recommended to provide a key attribute with v-for whenever possible, unless the iterated DOM content is simple (i.e. contains no components or stateful DOM elements), or you are intentionally relying on the default behavior for performance gains.

The key binding expects primitive values - i.e. strings and numbers. Do not use objects as v-for keys. For detailed usage of the key attribute, please see the key API documentation.

v-for with a Component

This section assumes knowledge of Components. Feel free to skip it and come back later.

You can directly use v-for on a component, like any normal element (don’t forget to provide a key):

1
<MyComponent v-for="item in items" :key="item.id" />

However, this won’t automatically pass any data to the component, because components have isolated scopes of their own. In order to pass the iterated data into the component, we should also use props:

1
2
3
4
5
6
<MyComponent
  v-for="(item, index) in items"
  :item="item"
  :index="index"
  :key="item.id"
/>

The reason for not automatically injecting item into the component is because that makes the component tightly coupled to how v-for works. Being explicit about where its data comes from makes the component reusable in other situations.

Example of a Simple Todo List

This example of a simple todo list to see how to render a list of components using v-for, passing different data to each instance.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// App.vue

<script>
import TodoItem from './TodoItem.vue'
  
export default {
  components: { TodoItem },
  data() {
    return {
      newTodoText: '',
      todos: [
        {
          id: 1,
          title: 'Do the dishes'
        },
        {
          id: 2,
          title: 'Take out the trash'
        },
        {
          id: 3,
          title: 'Mow the lawn'
        }
      ],
      nextTodoId: 4
    }
  },
  methods: {
    addNewTodo() {
      this.todos.push({
        id: this.nextTodoId++,
        title: this.newTodoText
      })
      this.newTodoText = ''
    }
  }
}
</script>

<template>
	<form v-on:submit.prevent="addNewTodo">
    <label for="new-todo">Add a todo</label>
    <input
      v-model="newTodoText"
      id="new-todo"
      placeholder="E.g. Feed the cat"
    />
    <button>Add</button>
  </form>
  <ul>
    <todo-item
      v-for="(todo, index) in todos"
      :key="todo.id"
      :title="todo.title"
      @remove="todos.splice(index, 1)"
    ></todo-item>
  </ul>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// TodoItem.vue

<script>
export default {
	props: ['title'],
  emits: ['remove']
}
</script>

<template>
  <li>
    {{ title }}
    <button @click="$emit('remove')">Remove</button>
  </li>
</template>

Simple Todo List

Array Change Detection

Mutation Methods

​Vue is able to detect when a reactive array’s mutation methods are called and trigger necessary updates. These mutation methods are:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

Replacing an Array

​Mutation methods, as the name suggests, mutate the original array they are called on. In comparison, there are also non-mutating methods, e.g. filter(), concat() and slice(), which do not mutate the original array but always return a new array. When working with non-mutating methods, we should replace the old array with the new one:

1
this.items = this.items.filter((item) => item.message.match(/Foo/))

You might think this will cause Vue to throw away the existing DOM and re-render the entire list - luckily, that is not the case. Vue implements some smart heuristics to maximize DOM element reuse, so replacing an array with another array containing overlapping objects is a very efficient operation.

Displaying Filtered/Sorted Results

​Sometimes we want to display a filtered or sorted version of an array without actually mutating or resetting the original data. In this case, you can create a computed property that returns the filtered or sorted array.

For example:

1
2
3
4
5
6
7
8
9
10
data() {
  return {
    numbers: [1, 2, 3, 4, 5]
  }
},
computed: {
  evenNumbers() {
    return this.numbers.filter(n => n % 2 === 0)
  }
}
1
<li v-for="n in evenNumbers">{{ n }}</li>

In situations where computed properties are not feasible (e.g. inside nested v-for loops), you can use a method:

1
2
3
4
5
6
7
8
9
10
data() {
  return {
    sets: [[ 1, 2, 3, 4, 5 ], [6, 7, 8, 9, 10]]
  }
},
methods: {
  even(numbers) {
    return numbers.filter(number => number % 2 === 0)
  }
}
1
2
3
<ul v-for="numbers in sets">
  <li v-for="n in even(numbers)">{{ n }}</li>
</ul>

Be careful with reverse() and sort() in a computed property! These two methods will mutate the original array, which should be avoided in computed getters. Create a copy of the original array before calling these methods:

1
2
- return numbers.reverse()
+ return [...numbers].reverse()