In this article, we will take a look at some common mistakes that developers new to Vue.js often make, and how they can be avoided to become a Vue.js pro.
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.
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.
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:
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 addProductToCart
method. 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: 'loreipsum@email.com',
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?
Cancel
button and revert typed changesAn 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:
this.form = {...this.user}
Save
the changesAccessing 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:
showMenu
or selectedOption
property. The dropdown will fail to close and no option will be selecteddropdown-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.
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.
#vue-js #javascript