Vue.js: 3 anti-patterns

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 message


Incorrect order value calculation for a special customer

In 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:


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."

 //   <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() {     //            }  } } //   UserForm.vue <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 get started with user using the v-model directive. Vue.js allows this. Why not do just that? Think about it:


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:


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.

 // Dropdown.vue ( ) <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 ( ) <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 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:


 // Dropdown.vue ( ) <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> 

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.

 // Dropdown.vue ( ) <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 ( ) <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)    }  } } 

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?

Source: https://habr.com/ru/post/463327/


All Articles