A Recipe for Nesting Components Within a List in Vue.js and Vuex

Blunt Jackson
4 min readNov 11, 2018

--

So, this situation baffled me for a while. In my state object, I have a list of keys, and then I have an object that contains the data for all the keys. It looks like so:

The first question a sensible person might ask is: why use this data structure at all? Why not simply have a list of items?

In my real world scenario, I have multiple lists. I want to be able to re-order the lists, move items from one list to another, create items, edit items, etc. All of this becomes at least conceptually easier when an item can be referred to by its id, rather than finding it in the correct list and referencing it by list-and-position. The items object is the authoritative model of data; the list array is simply a tool of convenience.

All well and good; now I want to display a list of my items. My first component, then is the list component itself:

With a root level element that looks like the following, this works like a champ.

<event-list :list="this.$store.state.list"></event-list>

In the markup, Vue’s :list directive instructs Vue to make the state.list data available to the property list which is defined in the component as requiring an Array. All good!

Where I ran into trouble was in desiring to iteratively include a component for each list item which would display the actual item data! What are the correct ways to get from the key in the array to the full item in a nested component?

I’ll spare you all the horrible syntactical convolutions that I tried and failed with. Here’s the short, sweet answer (although not quite the best one, as you shall see).

Here’s the component itself:

The two crucial points of wiring are the prop for ikey which is where we are going to receive the key from the parent component, the list itself, and the data section that locates the item in our store, given that ikey. You might wonder why I am using ikey instead of key. I initially tried to use key, but Vue threw errors. It is a reserved word in Vue. So I renamed my property and moved on. But this is going to come back very significantly later.

We now need to change the template in our list to properly reference this component, and complete the circuit. Now:

<ul>
<li v-for='key in list'>
<event-list-item :ikey="key"></event-list-item>
</li>
</ul>

As before, we tell Vue to allocate the value of the key our parent component is using for the property ikey, which our component code requires and expects as a String; the component’s data function then uses that property to obtain the authoritative record.

As easy as that, the job is done.

But it turns out there is something missing, once our application gets more complicated.

I added functionality to delete an item from our list. I won’t go into all that code, for the most part it is easy and obvious. But I encountered a baffling bug. The item would be correctly removed from the list, and the list display would update with an item removed… but not (necessarily) the right item!

The biggest mystery in this was that taking an action on the item that I thought I was removing revealed it to be an item that was not removed! Somehow the list display had become unmoored from the underlying data source.

Which, as you may imagine, is where that :key keyword comes in.

Revise our list template code one more time:

<ul>
<li v-for='key in list'>
<event-list-item :key="key" :ikey="key"></event-list-item>
</li>
</ul>

Now, any actions taken on the underlying list correctly update the list and the display of items. Voila!

There’s just one little thing… do we really need both a key and an ikey? No, no we don’t. But key takes a little extra magic to access from within the component. Let’s update the template and the component one last time, and for full effect, put it all in a single single-file fully working example. Because gist hates me and won’t tab correctly, you get this one without syntax highlighting.

<DOCTYPE html>
<html>
<head>
<title>Test Vue List</title>
<meta charset='utf-8' />
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vuex"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
<div id="events" class="main">
<h1>Event List</h1>
<event-list :list="this.$store.state.list" />
</div>
<script>
var mapState = Vuex.mapStateconst store = new Vuex.Store({
state: {
list: ["key1", "key2", "key3"],
items: {
key1: {title: "Title 1"},
key2: {title: "Title 2"},
key3: {title: "Title 3"},
}
}
})
Vue.component('event-list', {
template: "<ul><li v-for='key in list'><event-list-item :key=\"key\" /></li></ul>",
props: {
list: {
type: Array,
required: true
}
}
})
Vue.component('event-list-item', {
template: "<h4>{{ item.title }}</h4>",
data: function() {
return {
item: store.state.items[this.$vnode.key]
}
}
})
var app = new Vue({
el: '#events',
store
})
</script>
</body>
</html>

The final trick, then is to dispense with the property entirely and reference key not as a property, but as a Vue magic variable, off the $vnode object.

Please let me know if this was of benefit to you, or if you see any ways it can be made more elegant!

Isn’t it nice to code in a coffee shop? My favorite. None of these people are me. It’s a stock photo.

Other Articles in the Vue Series:

--

--

Blunt Jackson

Building web applications since 1992. Crikey, that’s a long time.