How & Why to use the `:key` attribute in VueJS v-for loops
Tags: vue, javascript
Published: 2021-02-11 19:15:15

When developing in VueJS, you might have realized that you should use **the `:key` attribute in the `v-for` loops**. You also might have ended up with a linter like ESLint screaming at you, stating you should always use the `:key` attribute with `v-for`.<br/>
So you thought, _well why not just use the index of the looped array_. Great, the linter stopped complaining. 

But using `:key` like that is just dead wrong.

In this article, I'll explain why and how you should use the VueJS `:key` attribute in `v-for` loops.

Check the video I made on this topic:

{% youtube lPs0uh5oR4k %}

Or just keep reading to learn more.

## How VueJS renders data in HTML templates
Let’s see how, **conceptually speaking**, VueJS renders changes in data.

Say you have the following in your template:

```html
<h1>{{ blogPostTitle }}</h1>
```

Now imagine that the value of the variable `blogPostTitle` changes to another value. By default, VueJS is going to optimize this operation by _patching_ the `<h1>` element, meaning that it’s going to **modify the value (content) of the element** that’s already there.

## ‘key’ special attribute in VueJS
In simple cases like this, patching the element _in-place_ like described above is exactly what we want, but for certain other cases we want to give an extra “hint” to the VueJS _virtual DOM_ algorithm to avoid patching, and **instead recreate the whole DOM element**, i.e. delete and create.

So, for that purpose, we use the `:key` attribute. If we do the following:

```html
<h1 :key="blogPostTitle">{{ blogPostTitle }}</h1>
```

This tells the algorithm that whenever the `key` for the `<h1>` changes (which is set to `blogPostTitle`), it should completely recreate the whole element. In this example, it’s each time the value of the variable `blogPostTitle` itself changes.

For simple cases like this, it doesn’t make sense to use `:key`. But, there are more complex cases where if you don’t use the `:key`, you’re going to run into bugs.
For example, adding the `:key` when using `v-for` loops inside templates is very important, especially if we’re looping over Vue components that have initialization logic.

## What can happen if we don’t use ‘:key’ in v-for ?
To prove this point, let’s start with an example. We have an app that renders a list of to-do items.

```html
<div id="app">
  <todo-item v-for="todo in todos" :item="todo"> 
  </todo-item>
  
  <button @click="addTodo">
    Add new item
  </button>
</div>
```

The Vue app is defined like this:

```js
new Vue({
  el: "#app",
  methods: {
  	addTodo() {
    	this.todos.unshift(
      	  { id: 2000, text: 'Write article' },
      );
    },
  },
  data() {
	return {
      todos: [
        { id: 1000, text: "Decide on article topic" },
        { id: 1001, text: "Prepare code example" },
        { id: 1002, text: "Prepare article outline" },
      ],
	};
  },
})
```

We have a predefined list of `todos` and a method `addTodo()` which adds a new to-do item to the beginning of the `todos` array. For simplicity, I hard-coded the new item `{ id: 2000, text: 'Write article' }`, but it could be added via button and a text input field. As you can see from the app template above, the `addTodo()` gets called when we click the button `Add new item`.

To render a list of to-do items themselves, we use a Vue component `todo-item` defined like this:

```js
Vue.component('todo-item', {
  props: {
  	item: {
      type: Object,
      required: true,
    },
  },
  data() {
  	return {
          isDone: false,
  	};
  },
  template: `
  	<div>
      <input type="checkbox" :id="item.id" v-model="isDone">
      <label :for="item.id">{{ item.text }}</label>
    </div>
  `
})
```

Notice we initialize the component’s local `isDone` variable to false since we want to make every newly added to-do item to be “not done”.  Once we first open the app in the browser, we see the following:

![3 todo items unticked](https://dev-to-uploads.s3.amazonaws.com/i/uf4tfg6j0zce0xy8qpis.png)

Now let’s say we tick all of the items “done”:


![All 3 todo items ticked](https://dev-to-uploads.s3.amazonaws.com/i/37lcrwohm8xr9vt3cnsd.png)


This changes each of the component’s `isDone` variable to `true`.

Now we click the “Add new item” button to add a new “Write article” to-do to the top, and we might be surprised to find this:


![first 3 todo items ticket, 4th unticked](https://dev-to-uploads.s3.amazonaws.com/i/q1s0ixbr3jovk4yrq56u.png)

**So what happened here ?!?** We were expecting that the old items remained ticked as “done” and the new one initialized to “not done”.

The issue lies in the fact that VueJS is **optimizing changes to the DOM and reusing the existing elements** (patching them) as much as possible.
The new item was added to the top in the same position as the item “Decide on article topic” was placed before the new item was added. So instead of a new element being created, the existing one was patched with the new “Write article” item.

Because the old DOM element was reused and the new one was **not created**, the component was **not** initialized with `isDone: false` and the existing value of `isDone` which is set to `true` is used. If we had any logic in the Vue hooks like `created()` or `mounted()`, those would not run either.

On the other hand, the “Prepare article outline” item was “pushed” off the bottom and ended up in a new position that didn’t exist before in the DOM. So the element (component) was re-created and `isDone` was initialized to `false`.
Again, this was not our intention.

## Solution: use the `:key`
These kind of bugs are very difficult to debug. That’s why we should always use the `:key` attribute with `v-for`:

```html
<div id="app">
  <todo-item v-for="todo in todos" :item="todo" :key="todo.id"> 
  </todo-item>
  
  <button @click="addTodo">
    Add new item
  </button>
</div>
```

As you can see, we used the `id` of each to-do item as the key. This way, since the id is unique for each to-do item, we are effectively “showing” the algorithm which items were there before the change, and which one was added.

Since the new to-do item has an `id=2000`, Vue knows that it’s a new item that didn’t exist before so it creates a new component to hold it and its `isDone` is initialized to `false`. For this simple example with hard-coded value `{ id: 2000, text: 'Write article' }`, the button should be clicked only once, otherwise there would be **more items with the same id, and that is not allowed** either.

As for the rest of the existing to-do items, since they have the keys with the same `id` values as before we clicked the button, Vue knows it should keep the existing components and place them in new positions. That’s why their `isDone` values remain unchanged.

Now we get the result we originally expected:

![First todo item unticked, final 3 ticked](https://dev-to-uploads.s3.amazonaws.com/i/k1do6xc2bl136gk02xkp.png)

## Common misconception
Developers often get tripped up on thinking that since the `:key` is strongly recommended (even required by some ESLint rules) in the `v-for` loops, that they can just use the index of the loop as `:key`.

l know! I was there 😊

Sure, it’s going to get rid of the ESLint warning, but the problem with that approach is that it’s exactly the same as *not providing a `:key` attribute*, because the uniqueness of items cannot be based on loop indices since those can be reused for new values.

That’s why it’s important to use a unique value like `id` for the `:key`.

## Conclusion
Alright, folks, I hope this was clear enough, feel free to ask any questions in the comments and maybe we can all learn something we didn’t know yet 😉.