How to Write Better Vue v-for Loops

In Vue, v-for loops are something that every project will use at some point or another. They allow you to write for loops in your template code and create elements iteratively.

v-for is most commonly used to render items in a list, but can also be used to iterate over an object's properties or even a set range.

When iterating over an array, Vue v-for loops look like this.

Foo.vue
<template>
  <div v-for="product in products">
    {{ product.name }}
  </div>
</template>

In this article, we’ll be covering some ways to make your v-for loops more precise, predictable, and efficient.

Accessing index in v-for

In addition to accessing our values, we can also keep track of the index of each item.

To do this, we have to add an `index`` value after our item. It’s simple, and is useful for things like pagination, displaying the index of a list, showing rankings, etc.

Like other indices in programming, index starts at 0.

App.vue
<script setup lang="ts">
import { ref } from 'vue'

const products = ref([
  {id: 0, name: 'shirt'},
  {id: 1, name: 'jacket'},
  {id: 2, name: 'shoes'}
])
</script>
<template>
  <div 
    v-for='(product, index) in products' 
    :key='product.id' 
  >
    #{{ index + 1}} - {{ product.name }}
  </div>
</template>

Use key in your Vue v-for loops

A common Vue best practice is to use :key in your v-for loops.

key is a special attribute that lets us give hints for Vue's rendering system to identify specific virtual nodes.

If we don't provide a unique key, Vue will try to minimize element movement when updating the DOM.

For example, if we want to change the order of our list, instead of reordering the DOM elements - Vue will make edits to the elements in-place to reflect the data change. This is called an in-place patch.

If we have a unique key reference for each element (usually some unique id), Vue's rendering system will reorder elements instead of performing in-place patches.

Do we really need key though?

Two cases where a missing key can cause problems in our v-for are when our v-for has:

  • stateful DOM elements (like form inputs)
  • when rendering components

Like we saw, Vue's default rendering avoids moving DOM elements, so if we had a child component or form input, it doesn't move with our product when we reorder our list. Instead, each product.name gets patched with the new product - but the input stays in place.

To fix this, we need to tell Vue that each li generated by our v-for is tied to a specific product and all of its content (including the input) should move when our products get order.

You guessed it - that's where we can use key.

The same thing applies if our iterated content contains a child component. Without a key, it will stay in place when we reorder our list.

But don't use index as the key!

The purpose of specifying a key is for Vue to link each item in our list to its corresponding vnode. Then, when the orders of our keys change - the orders of the elements will as well.

If we use index as our key, the order of the keys will never change - since index always increases sequentially. This acts the same as not declaring a key since the index will be the same at each element's place.

Should you always use key?

It's a best practice to add the key attribute to your v-for loops whenever you can. By always using it, you can avoid debugging the cases where it's essential (components and stateful DOM elements).

Also, cases where we want list movement to be animated using <TransitionGroup> requires a key since we will be moving our elements.

In cases where your iterated content is simple, key won't always impact the functionality of your v-for, but it's still recommended to get in the habit of always adding one. Then, you can make exceptions on a case-by-case basis.

Using v-for to loop over a range

While most of the time v-for is used to loop over arrays or objects, there are definitely cases where we might just want to iterate a certain number of times.

For example, let’s say we are creating a pagination system for an online store and we only want to show 10 products per page. Using a variable to track our current page number, we could handle pagination like this.

<ul>
  <li v-for="index in 10" :key="index">{{ products[page * 10 + index] }}</li>
</ul>

Avoid use v-if in loops

A common mistake is improperly using v-ifto filter the data that we are looping over with our v-for.

Although this seems intuitive, it can cause performance issues because Vue prioritizes the v-for over the v-if directive.

So, your component will loop through every element in our list and THEN check the v-if conditional to see if it should render.

If you use v-if with v-for, you’ll be iterating through every item of your array no matter what your conditional is.

<!--BAD-->
<ul>
  <li v-for="product in products" :key="product._id" v-if="product.onSale">
    {{ product.name }}
  </li>
</ul>

So what’s the issue?

Let’s say our products array has 1000 items, but only 3 of them are on sale and should be rendered.

Every time our component re-renders, Vue will loop over all 1000 products even if the 3 products on sale didn't change at all.

Let’s look at two alternatives to this anti-pattern.

Use computed properties or a method instead

We can filter our data before iterating over it in our template. There are two very similar ways to do this:

The way you choose to do this is up to you, so let’s just cover both real quick.

First, we have to set up a computed property. To get the same functionality as our earlier v-if, the code would look like this.

<template>
  <ul>
    <li v-for="products in productsOnSale" :key="product._id">
      {{ product.name }}
    </li>
  </ul>
</template>

<script>
export default {
  data() {
    return {
      products: [],
    }
  },
  computed: {
    productsOnSale: function () {
      return this.products.filter((product) => product.onSale)
    },
  },
}
</script>

This has a few benefits:

  • Our data property will only be re-evaluated when a dependency changes
  • Our template will only loop over the products on sale, instead of every single product

The code for the method is pretty much the same, but using a method changes how we access the values in our template. However, if we need to pass in a variable into our filter (let's say productsOnSale is used multiple times with different price points), methods are the way to go.

<template>
  <ul>
    <li v-for="products in productsOnSale(50))" :key="product._id">
      {{ product.name }}
    </li>
  </ul>
</template>

<script>
export default {
  data() {
    return {
      products: [],
    }
  },
  methods: {
    productsOnSale(maxPrice) {
      return this.products.filter(
        (product) => product.onSale && product.price < maxPrice
      )
    },
  },
}
</script>

Or surround your loop with a wrapper element

Another time you may want to join v-for with v-ifis when deciding whether or not to render a list at all.

For example, what if we only want to render our products list when a user is logged in.

<!--BAD-->
<ul>
  <li
    v-for="product in products"
    :key="product._id"
    v-if="isLoggedIn"
  >
    > {{ product.name }}
  </li>
</ul>

What’s wrong with this?

Same as before. Our Vue template will prioritize the v-for – so it will loop over every element and check the v-if.

So even if our list renders nothing, we’re going to loop over thousands of elements.

The easy solution here is to move our v-if statement.

<!--GOOD-->
<ul v-if="isLoggedIn">
  <!-- Much better -->
  <li v-for="product in products" :key="product._id">{{ product.name }}</li>
</ul>

This is much better because if isLoggedIn is false – we’re not going to have to iterate at all.

Iterating over an object

So far, we’ve only really looked at using v-for to iterate through an array. But we can just as easily iterate over an object’s key-value pairs.

Similar to accessing an element’s index, we have to add another value to our loop. If we loop over an object with a single argument, we’ll be looping over all of the items.

If we add another argument, we’ll get the items AND keys. And if we add a third, we can also access the index of the v-for loop.

With everything, it would look like this.Let’s just say we want to loop over each property in our products.

<ul>
  <li v-for='(products, index) in products' :key='product._id' >
    <span v-for='(item, key, index) in product' :key='key'>
      {{ item }}
    </span>
  </li>
</ul>

Conclusion

Hopefully, this quick article taught you some of the best practices about using Vue’s v-for directive.

What other tips do you have? Let me know in the replies!!

Happy coding 🙂