Vue.js is probably one of the nicest JavaScript frameworks. It has an intuitive API, it is fast, flexible, and easy to use. However, the flexibility of Vue.js comes with certain dangers. Some developers working with this framework are prone to small oversights. This can adversely affect application performance, or, in the long run, the ability to support them.
The author of the material, the translation of which we publish today, offers to parse some common mistakes made by those who develop applications on Vue.js.
Side effects inside computed properties
Computed properties are a very convenient Vue.js mechanism that allows you to organize work with state fragments that depend on other state fragments. Computed properties should only be used to display data stored in a state that depends on other data from the state. If it turns out that you call some methods inside the calculated properties or write some values to other state variables, this may mean that you are doing something wrong. Consider an example.
export default { data() { return { array: [1, 2, 3] }; }, computed: { reversedArray() { return this.array.reverse();
If we try to infer
array
and
reversedArray
, we will notice that both arrays contain the same values.
: [ 3, 2, 1 ] : [ 3, 2, 1 ]
This is because the computed
reversedArray
property modifies the original
array
property by calling its
.reverse()
method. This is a fairly simple example that demonstrates unexpected system behavior. Take a look at another example.
Suppose we have a component that displays detailed information about the price of goods or services included in a certain 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); } } }
Here we created a computed property that displays the total cost of the order, including taxes and discounts. Since we know that the total order value is changing here, we can try to raise an event that notifies the parent component of a
grandTotal
change.
<price-details :order="order" @total-change="totalChange"> </price-details> export default { // methods: { totalChange(grandTotal) { if (this.isSpecialCustomer) { this.order = { ...this.order, discount: this.order.discount + 0.1 }; } } } };
Now imagine that sometimes, although very rarely, situations arise in which we work with special customers. We give these customers an additional 10% discount. We can try to change the
order
object and increase the discount size by adding
0.1
to its
discount
property.
This, however, will lead to a bad mistake.
Error messageIncorrect order value calculation for a special customerIn a similar situation, the following occurs: the calculated property is constantly, in an infinite loop, “recounted”. We change the discount, the calculated property reacts to this, recalculates the total cost of the order and generates an event. When processing this event, the discount increases again, this causes a recalculation of the calculated property, and so on - to infinity.
It may seem to you that such a mistake cannot be made in a real application. But is it really so? Our script (if something like this happens in this application) will be very difficult to debug. Such a mistake will be extremely difficult to track. The fact is that for this error to occur, it is necessary that the order is made by a special buyer, and one such order may have 1000 regular orders.
Change nested properties
Sometimes a developer may be tempted to edit something in a property from
props
, which is an object or an array. Such a desire can be dictated by the fact that it is very “simple” to do it. But is it worth it? Consider 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--; } } } };
Here we have the
Product.vue
component, which displays the name of the product, its value and the quantity of goods that we have. The component also displays a button that allows the buyer to put the goods in the basket. It may seem that it will be very easy and convenient to decrease the value of the
product.stock
property after clicking on the button. To do this, and the truth is simple. But if you do just that, you may encounter several problems:
- We perform a change (mutation) of the property and do not report anything to the parent entity.
- This can lead to unexpected system behavior, or, even worse, to the appearance of strange errors.
- We introduce some logic into the
product
component, which probably should not be present in it.
Imagine a hypothetical situation in which another developer first encounters our code 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--; } } } };
This developer’s thinking may be as follows: “Apparently, I need to reduce
product.stock
in the
addProductToCart
method
addProductToCart
” But if this is done, we will encounter a small mistake. If now press the button, the quantity of goods will be reduced not by 1, but by 2.
Imagine that this is a special case when such a check is performed only for rare goods or in connection with the availability of a special discount. If this code gets into production, then everything can end up with the fact that our customers will, instead of 1 copy of the product, buy 2 copies.
If this example seemed unconvincing to you, imagine another scenario. Let it be the form that the user fills out. We pass the essence of
user
into the form as a property and are going to edit the name and email address of the user. The code shown below may appear to be "correct."
It’s easy to get started with
user
using the
v-model
directive. Vue.js allows this. Why not do just that? Think about it:
- What if there is a requirement that you need to add a Cancel button to the form, clicking on which cancels the changes made?
- What if a server call fails? How to undo
user
object changes? - Do we really want to display the changed name and email address in the parent component before saving the corresponding changes?
A simple way to “fix” the problem may be to clone the
user
object before sending it as a property:
<user-form :user="{...user}">
Although this may work, we only circumvent the problem, but do not solve it. Our
UserForm
component must have its own local state. Here is 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 } } } } } }
Although this code certainly seems rather complicated, it is better than the previous version. It allows you to get rid of the above problems. We expect (
watch
) changes to the
user
property and copy it into the internal
form
data. As a result, the form now has its own state, and we get the following features:
- You can undo the changes by reassigning the form:
this.form = {...this.user}
. - We have an isolated state for the form.
- Our actions do not affect the parent component in the event that we do not need it.
- We control what happens when we try to save changes.
Direct access to parent components
If a component refers to another component and performs some actions on it, this can lead to contradictions and errors, this can result in strange behavior of the application and in the appearance of related components in it.
Consider a very simple example - a component that implements a drop-down menu. Imagine that we have a
dropdown
component (parent) and a
dropdown-menu
component (child). When the user clicks on a certain menu item, we need to close the
dropdown-menu
. Hiding and showing this component is done by the parent component of
dropdown
. Take a look at an example.
Pay attention to the
selectOption
method. Although this happens very rarely, someone may want to directly contact
$parent
. This desire can be explained by the fact that it is very simple to do.
At first glance, it might seem that such code works correctly. But here you can see a couple of problems:
- What if we change the
showMenu
or selectedOption
property? The drop-down menu will not be able to close and none of its items will be selected. - What if you need to animate the
dropdown-menu
using some kind of transition?
This code, again, due to a change in
$parent
, will not work. The
dropdown
component is no longer the parent of the
dropdown-menu
. Now the
dropdown-menu
parent is the
transition
component.
Properties are passed down the component hierarchy, events are passed up. These words contain the meaning of the correct approach to solving our problem. Here is an example modified for events.
Now, thanks to the use of events, the child component is no longer bound to the parent component. We can freely change properties with data in the parent component and use animated transitions. However, we may not think about how our code can affect the parent component. We simply notify this component of what happened. In this case, the
dropdown
component itself makes decisions on how to handle the user's choice of a menu item and the operation of closing a menu.
Summary
The shortest code is not always the most successful. Development techniques involving “simple and quick” results are often flawed. In order to properly use any programming language, library or framework, you need patience and time. This is true for Vue.js.
Dear readers! Have you encountered any troubles in practice, similar to those discussed in this article?