Vue gotchas

Matus Peciar
Dec 10, 2018 · 5 min read

Clean up after yourself

Most of the times we use Vue with third-party plugins. Let’s say we want to mount a color picker library into our component. That usually means, we have to import the module and initialize it in the mounted() hook. It might look like this.

mounted() {
this.colorPicker = ColorPicker({
el: this.$refs.colorPicker,
onUpdate: this.updateHexValue
})
)

The mounted hook gets called when our component root element (accessible as this.$el) has been mounted to DOM. After that, we are able to access our referenced elements and manipulate them or, as in this instance, use them to initialize a color picker.

The gotcha with this is that after our component is destroyed, the ColorPicker instance is not. At least not properly. The reference to it is lost and hopefully garbage-collected, but since the time it was created, ColorPicker might have attached a few event listeners in our DOM which still exist.

All well-written libraries should provide an API method to destroy their initialized instances and clean up after themselves. We should check the documentation and confirm the library includes a method that does it. If it doesn’t, it’s time to find a different library.

All we need to do next is call this method in the beforeDestroy() hook.

beforeDestroy () {
this.colorPicker.destroy()
}

This also applies to timeouts and intervals. Don’t forget to call clearInterval() or clearTimeout() in your beforeDestroy() hook

To demonstrate this problem, I created a fiddle: https://jsfiddle.net/Horsetoast/nz5uh9ed/7/
Each component you add creates an event listener but doesn’t remove it.
Create a bunch, remove them and click anywhere in the window to see what’s happening.

The :key to success

If you’ve used Vue before you’ve most likely encountered this warning message

component lists rendered with v-for should have explicit keys

Why do we need to pass a key to v-for elements? To cite the documentation

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.

Why is this important? Because things can go wrong if we use “index” as the key.

<div v-for="(user, index) in users" :key="index">
{{ user.name }}
</div>

After we shuffle the order of our array, the indices will always remain the same (0, 1, 2, 3…) which really doesn’t affect us in this case if we are only displaying the user name.

However, there are cases when problems arise. Let’s try adding a text input.

<div v-for="(user, index) in users" :key="index">
{{ user.name }}
<input type="text"/>
</div>

When we shuffle the array of users now, the user names will get updated as they should but the inputs will stay in their original positions with the same values.

To solve this problem, we would pass a unique id to the key directive to tell Vue that we want to track our elements not by indices, but by ids. In order for this solution to work, we assume that our user objects contain an id property.

<div v-for="(user, index) in users" :key="user.id">
{{ user.name }}
<input type="text"/>
</div>

If you want to see the difference using index vs unique id as the key, check this fiddle https://jsfiddle.net/Horsetoast/Ldqrbup4/20/

Of course, if you bind the input to data with v-model, everything will work just fine. This example is just to demonstrate the caveat. Instead of inputs, you might have more complex components that have attached elements from a library that operates on DOM nodes in which case re-ordering the array could mess things up.

Using keys with transitions

I have often encountered a scenario when using the transition component for animations that use the same tag name.

I remember the first time I was confused as to why my transition was not working:

<transition name="fade">
<p v-if="step === 0">Start now</p>
<p v-else-if="step === 1">Step 1</p>
<p v-else-if="step === 2">Step 2</p>
</transition>

After some googling, I found out why. As stated in the documentation:

When toggling between elements that have the same tag name, you must tell Vue that they are distinct elements by giving them unique key attributes. Otherwise, Vue’s compiler will only replace the content of the element for efficiency. Even when technically unnecessary though, it’s considered good practice to always key multiple items within a component.

Try out the animations here https://jsfiddle.net/Horsetoast/6najed5z/

(Non)Reactive Data

All properties returned from the data() function are reactive by default. That means any time a piece of that data changes, all its dependent computed properties or render functions update.

Vue 2.x initializes the reactive properties by walking through each of them and creating custom getters and setters with Object.defineProperty(). Vue 3 uses Proxy objects to accomplish this.

Learn more about reactivity in Vue in this nicely illustrated article: https://www.vuemastery.com/courses/advanced-components/build-a-reactivity-system/

However, there are cases where we can’t initialize the data beforehand because we don’t know its structure (Some people might object that if we can’t predict our data structure then we’re doing something wrong, but let’s not dive into that discussion now). Imagine a scenario where we need to fetch the data of our users which comes as an array of objects.

{
"id": "p18Af0",
"name": "Chris"
},
{
"id": "lqR55z",
"name": "Sarah"
}

But right after we fetch them, we want to store them as an object using ids as the keys and names as the values. Why store them in this way? Maybe we’re trying to be efficient and have a map of users instead of an array. Therefore we don’t need to loop through it every time we search for a user by id.

{
"p18Af0": "Chris",
"lqR55z": "Sarah"
}

Our code might look like this.

const users = await api.getUsers();
users.forEach(user => {
this.users[user.id] = user.name;
})

Except that it wouldn’t work. Since the new properties of the this.users don’t have defined getters and setters they wouldn’t be reactive, thus Vue wouldn’t know when to update.

An easy solution to this would be to use this.$set() function

const users = await api.getUsers();
users.forEach(user => {
this.$set(this.users, user.id, user.name);
})

However, the preferred solution would be to completely replace the this.users object with a new one rather than calling the this.$set() function for each item.

const users = await api.getUsers();
const tempUsers = {};
users.forEach(user => {
tempUsers[user.id] = user.name;
});
this.users = tempUsers;

Demonstration https://jsfiddle.net/Horsetoast/g08usjoL/