Rendering a list of Vue components
At this point we've got a fully working component; we're now ready to add multiple ToDoItem components to our app. In this article we'll look at adding a set of todo item data to our App.vue component, which we'll then loop through and display inside ToDoItem components using the v-for directive.
| Prerequisites: |
Familiarity with the core HTML, CSS, and JavaScript languages, knowledge of the terminal/command line. Vue components are written as a combination of JavaScript objects that manage the app's data and an HTML-based template syntax that maps to the underlying DOM structure. For installation, and to use some of the more advanced features of Vue (like Single File Components or render functions), you'll need a terminal with node + npm installed. |
|---|---|
| Objective: | To learn how to loop through an array of data and render it in multiple components. |
Rendering lists with v-for
To be an effective to-do list, we need to be able to render multiple to-do items. To do that, Vue has a special directive, v-for. This is a built-in Vue directive that lets us include a loop inside of our template, repeating the rendering of a template feature for each item in an array. We'll use this to iterate through an array of to-do items and display them in our app in separate ToDoItem components.
Adding some data to render
First we need to get an array of to-do items. To do that, we'll add a data property to the App.vue component object, containing a ToDoItems field whose value is an array of todo items. While we'll eventually add a mechanism to add new todo items, we can start with some mock to do items. Each to-do item will be represented by an object with a label and a done property.
Add a few sample to-do items, along the lines of those seen below. This way you have some data available for rendering using v-for.
export default {
name: "app",
components: {
ToDoItem,
},
data() {
return {
ToDoItems: [
{ label: "Learn Vue", done: false },
{ label: "Create a Vue project with the CLI", done: true },
{ label: "Have fun", done: true },
{ label: "Create a to-do list", done: false },
],
};
},
};
Now that we have a list of items, we can use the v-for directive to display them. Directives are applied to elements like other attributes. In case of v-for, you use a special syntax similar to a for...in loop in JavaScript — v-for="item in items" — where items is the array you want to iterate over, and item is a reference to the current element in the array.
v-for attaches to the element you want to repeat, and renders that element and its children. In this case, we want to display an <li> element for every to-do item inside our ToDoItems array. Then we want to pass the data from each to-do item to a ToDoItem component.
Key attribute
Before we do that, there's one other piece of syntax to know about that is used with v-for, the key attribute. To help Vue optimize rendering the elements in the list, it tries to patch list elements so it's not recreating them every time the list changes. However, Vue needs help. To make sure it is re-using list elements appropriately, it needs a unique "key" on the same element that you attach v-for to.
To make sure that Vue can accurately compare the key attributes, they need to be string or numeric values. While it would be great to use the name field, this field will eventually be controlled by user input, which means we can't guarantee that the names would be unique. We can use nanoid(), however, as we did in the previous article.
-
Import
nanoidinto yourAppcomponent in the same way you did with yourToDoItemcomponent, usingjsimport { nanoid } from "nanoid"; -
Next, add an
idfield to each element in yourToDoItemsarray, and assign each of them a value of`todo-${nanoid()}`.The
<script>element inApp.vueshould now have the following contents:jsimport { nanoid } from "nanoid"; import ToDoItem from "./components/ToDoItem.vue"; export default { name: "app", components: { ToDoItem, }, data() { return { ToDoItems: [ { id: `todo-${nanoid()}`, label: "Learn Vue", done: false }, { id: `todo-${nanoid()}`, label: "Create a Vue project with the CLI", done: true, }, { id: `todo-${nanoid()}`, label: "Have fun", done: true }, { id: `todo-${nanoid()}`, label: "Create a to-do list", done: false, }, ], }; }, }; -
Now, add the
v-fordirective andkeyattribute to the<li>element in yourApp.vuetemplate, like so:html<ul> <li v-for="item in ToDoItems" :key="item.id"> <to-do-item label="My ToDo Item" :done="true"></to-do-item> </li> </ul>When you make this change, every JavaScript expression between the
<li>tags will have access to theitemvalue in addition to the other component attributes. This means we can pass the fields of our item objects to ourToDoItemcomponent — just remember to use thev-bindsyntax. This is really useful, as we want our todo items to display theirlabelproperties as their label, not a static label of "My Todo Item". In addition, we want their checked status to reflect theirdoneproperties, not always be set todone="true". -
Update the
label="My ToDo Item"attribute to:label="item.label", and the:done="true"attribute to:done="item.done", as seen in context below:html<ul> <li v-for="item in ToDoItems" :key="item.id"> <to-do-item :label="item.label" :done="item.done"></to-do-item> </li> </ul>
Now when you look at your running app, it'll show the todo items with their proper names, and if you inspect the source code you'll see that the inputs all have unique ids, taken from the object in the App component.

Chance for a slight refactor
There's one little bit of refactoring we can do here. Instead of generating the id for the checkboxes inside your ToDoItem component, we can turn the id into a prop. While this isn't strictly necessary, it makes it easier for us to manage since we already need to create a unique id for each todo item anyway.
- Add a new prop to your
ToDoItemcomponent —id. - Make it required, and make its type a
String. - To prevent name collisions, remove the
idfield from yourdataattribute. - You are no longer using
nanoid, so you need to remove theimport { nanoid } from 'nanoid';line, otherwise your app will throw an error.
The <script> contents in your ToDoItem component should now look something like this:
export default {
props: {
label: { required: true, type: String },
done: { default: false, type: Boolean },
id: { required: true, type: String },
},
data() {
return {
isDone: this.done,
};
},
};
Now, over in your App.vue component, pass item.id as a prop to the ToDoItem component. Your App.vue template should now look like this:
<template>
<div id="app">
<h1>My To-Do List</h1>
<ul>
<li v-for="item in ToDoItems" :key="item.id">
<to-do-item
:label="item.label"
:done="item.done"
:id="item.id"></to-do-item>
</li>
</ul>
</div>
</template>
When you look at your rendered site, it should look the same, but our refactor now means that our id is being taken from the data inside App.vue and passed into ToDoItem as a prop, just like everything else, so things are now more logical and consistent.
Summary
And that brings us to the end of another article. We now have sample data in place, and a loop that takes each bit of data and renders it inside a ToDoItem in our app.
What we really need next is the ability to allow our users to enter their own todo items into the app, and for that we'll need a text <input>, an event to fire when the data is submitted, a method to fire upon submission to add the data and rerender the list, and a model to control the data. We'll get to these in the next article.