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?






All Articles