Vue.js is probably one of the most enjoyable Javascript libraries to work with. It has an intuitive api, it’s fast, easy to use and flexible. However, along with flexibility some developers tend to fall into small traps that might have a negative impact on the application performance or long term maintenance.
So let’s dive in and see what are some common mistakes that should be avoided when developing with Vue.js.
Side effects inside computed properties
Computed properties in Vue.js are a very convenient way to manage state that depends on other state. Computed properties should be used to only display state that depends on other state. If you find yourself calling other methods or assigning other properties inside computed then you’re most likely doing something wrong. Let’s take an example:
export default {
data() {
return {
array: [1, 2, 3]
};
},
computed: {
reversedArray() {
return this.array.reverse(); // SIDE EFFECT - mutates a data property
}
}
};
If we try to display the array and the reversedArray , you’ll notice that both arrays have the same values
original array: [ 3, 2, 1 ]
computed array: [ 3, 2, 1 ]
So what happened is that the computed property reversedArray modified the original array property because of the reverse function. This is a rather simple example which results in unexpected behavior.
Let’s look at another example:
Assume we have a component that displays price details of an order
export default {
props: {
order: {
type: Object,
default: () => ({})
}
},
computed:{
grandTotal() {
let total = (this.order.total + this.order.tax) * (1 - this.order.discount);
this.$emit('total-change', total)
return total.toFixed(2);
}
}
}
We created a computed that displays the total price including taxes and discounts. Since we know that the total price changed here, we might be tempted to emit an event upwards to notify the parent component of the grandTotal change.
<price-details :order="order"
@total-change="totalChange">
</price-details>
export default {
// other properties which are not relevant in this example
methods: {
totalChange(grandTotal) {
if (this.isSpecialCustomer) {
this.order = {
...this.order,
discount: this.order.discount + 0.1
};
}
}
}
};
Now let’s assume that in a very rare case when one of our customers is special, we want to give him an extra 10% discount. We might be tempted to modify the order and increase its discount.
This, however will result in a pretty bad error
What actually happens in this case is that, our computed property get’s “re-computed” every time in an infinite loop. We change the discount, the computed property picks this change and recalculates this total and emits the event back. The the discount is again increased which triggers another computed recalculation and so on infinitely.
You might think that it would be impossible to do such a mistake in a real application, but is it ? Our scenario (if it happened), would be really hard to debug or trace because it requires a “special customer” which may appear only once in every 1000 orders.
Mutating nested props
Sometimes it might be tempting to edit a property in a prop that is an object or an array, because it’s “Easy” to do so. But is it the best thing to do ? Let’s look at an example
<template>
<div class="hello">
<div>Name: {{product.name}}</div>
<div>Price: {{product.price}}</div>
<div>Stock: {{product.stock}}</div>
<button @click="addToCart" :disabled="product.stock <= 0">Add to card</button>
</div>
</template>
export default {
name: "HelloWorld",
props: {
product: {
type: Object,
default: () => ({})
}
},
methods: {
addToCart() {
if (this.product.stock > 0) {
this.$emit("add-to-cart");
this.product.stock--;
}
}
}
};
We have a Product.vue component which displays the product name, price and stock. It also contains a button to add the product to the cart. When clicking the button, it might be tempting to directly decrease the product.stock property. It’s easy to do so. However this can create couple of issues:
- We mutate the prop without letting the parent now about it
- We might get unexpected behavior or even worse, strange bugs because of this.
- We introduce some logic in the product component which probably shouldn’t be there
Let’s assume a hypothetical case when another dev look over the code for the first time and sees the parent component.
<template>
<Product :product="product" @add-to-cart="addProductToCart(product)"></Product>
</template>
import Product from "./components/Product";
export default {
name: "App",
components: {
Product
},
data() {
return {
product: {
name: "Laptop",
price: 1250,
stock: 2
}
};
},
methods: {
addProductToCart(product) {
if (product.stock > 0) {
product.stock--;
}
}
}
};
The dev might be tempted to think. Well, I should decrease the stock inside the addProductToCartmethod. By doing so, we introduce a small bug.
Now if we press the button, the quantity decreases by 2 instead of 1.
Imagine this is a special case, where such a check is made for special products/discounts and this code gets into a production environment. We might end up with users buying 2 products instead of 1.
If this doesn’t convince you, let’s assume another scenario. Let’s take the case of a user form for example. We pass in the user as a prop and want to edit its email and name. The code below might seem “right”
// Parent
<template>
<div>
<span> Email {{user.email}}</span>
<span> Name {{user.name}}</span>
<user-form :user="user" @submit="updateUser"/>
</div>
</template>
import UserForm from "./UserForm"
export default {
components: {UserForm},
data() {
return {
user: {
email: '[email protected]',
name: 'Lorem Ipsum'
}
}
},
methods: {
updateUser() {
// Send a request to the server and save the user
}
}
}
// UserForm.vue Child
<template>
<div>
<input placeholder="Email" type="email" v-model="user.email"/>
<input placeholder="Name" v-model="user.name"/>
<button @click="$emit('submit')">Save</button>
</div>
</template>
export default {
props: {
user: {
type: Object,
default: () => ({})
}
}
}
It’s easy to add v-model on the user. Vue allows that. So why not do it?
- What if we have a requirement to add a Cancel button and revert typed changes
- What if our server call fails. How do we revert the changes on the user ?
- Do we really want to show the changed email and name in the parent component before saving these changes ?
An easy “fix” might be to simply clone the user before sending it as a prop
<user-form :user="{...user}">
While this might work, it’s only a work around for the real problem. Our UserForm should have its own local state. Here’s what we can do.
<template>
<div>
<input placeholder="Email" type="email" v-model="form.email"/>
<input placeholder="Name" v-model="form.name"/>
<button @click="onSave">Save</button>
<button @click="onCancel">Save</button>
</div>
</template>
export default {
props: {
user: {
type: Object,
default: () => ({})
}
},
data() {
return {
form: {}
}
},
methods: {
onSave() {
this.$emit('submit', this.form)
},
onCancel() {
this.form = {...this.user}
this.$emit('cancel')
}
}
watch: {
user: {
immediate: true,
handler: function(userFromProps){
if(userFromProps){
this.form = {
...this.form,
...userFromProps
}
}
}
}
}
}
While the code above definitely feels more verbose, it’s better and avoids the issues described above. We watch for the user prop changes and then copy it to our own local form inside data. This allows us to have an individual state for the form and:
- Decide to cancel the changes by re-assigning the form this.form = {...this.user}
- Have isolated state for the form
- Not affect the parent unless we want to
- Have control when we Save the changes
Directly accessing parent components
Accessing and doing operations on other components other than the component itself can lead to inconsistencies, bugs, strange behaviors and coupled components.
We’ll take a very simple case of a dropdown component. Let’s assume we have a dropdown (parent) and dropdown-menu (child). When the user clicks a certain option, we’d like to close the dropdown-menu which is shown/hidden from the parent dropdown. Let’s see an example
// Dropdown.vue (parent)
<template>
<div>
<button @click="showMenu = !showMenu">Click me</button>
<dropdown-menu v-if="showMenu" :items="items"></dropdown-menu>
</div>
<template>
export default {
props: {
items: Array
},
data() {
return {
selectedOption: null,
showMenu: false
}
}
}
// DropdownMenu.vue (child)
<template>
<ul>
<li v-for="item in items" @click="selectOption(item)">{{item.name}}</li>
</ul>
<template>
export default {
props: {
items: Array
},
methods: {
selectOption(item) {
this.$parent.selectedOption = item
this.$parent.showMenu = false
}
}
}
Pay attention to selectOption method. Although this is very rare, some people would be tempted to access the $parent directly because it’s easy.
The code would work fine at first sight but what if:
- We change the showMenu or selectedOption property. The dropdown will fail to close and no option will be selected
- We want to add a transition to the dropdown-menu
// Dropdown.vue (parent)
<template>
<div>
<button @click="showMenu = !showMenu">Click me</button>
<transition name="fade">
<dropdown-menu v-if="showMenu" :items="items"></dropdown-menu>
</dropdown-menu>
</div>
<template>
Again the code will fail because the $parent changed. The parent of dropdown-menu is no longer the dropdown component but the transition component.
Props down, events up is the right way. Here’s our example from above, modified to use events
// Dropdown.vue (parent)
<template>
<div>
<button @click="showMenu = !showMenu">Click me</button>
<dropdown-menu v-if="showMenu" :items="items" @select-option="onOptionSelected"></dropdown-menu>
</div>
<template>
export default {
props: {
items: Array
},
data() {
return {
selectedOption: null,
showMenu: false
}
},
methods: {
onOptionSelected(option) {
this.selectedOption = option
this.showMenu = true
}
}
}
// DropdownMenu.vue (child)
<template>
<ul>
<li v-for="item in items" @click="selectOption(item)">{{item.name}}</li>
</ul>
</template>
export default {
props: {
items: Array
},
methods: {
selectOption(item) {
this.$emit('select-option', item)
}
}
}
By using events we are no longer coupled to the parent component. We are free to change the data properties inside the parent component, add transitions and not think about how our code might affect the parent component. We simply notify the parent that an action happened. It's up to the Dropdown how to handle the option selection and close the menu.
Conclusion
The shortest code is not always the best and "easy and fast" ways can often have disadvantages. Every programming language, project or framework requires patience and time to use it right. The same applies for Vue. Write your code carefully and with patience.