Tech 11 minutes read

Top 5 VueJs tips for developers

Learning a new framework can be exhausting and overwhelming. All the documentation you have to read through, check out all used libraries on projects you are working on, investigate what is a good practice and what isn't. But, what’s important to know is, it’s all a normal part of the journey! And in times like that, your new best friends are the framework’s community! So to help a friend in need here are our top 5 tips that you’d wanna live by while writing VueJs!

Top 5 tips for VueJS developers

1. Encapsulate components to be reusable

If you are familiar with OOP principles, you know what we mean by “encapsulating” things. We, humans, crave organization. Organized things get processed easily and so little time is spent searching for everything you need. That also applies in code: we want the logical parts to be grouped. Everything that makes sense to be grouped, it darn well should be! The reason is simple, code is easily maintained that way. No one likes seeing a huge component with 1000 lines of code. Imagine having a bug there. Ouch…

If you are having trouble separating that huge component, try to plan it out on paper. Think about what responsibility each component should have. Think in advance how complicated or not it will be to communicate changes with parent/child components. Even that shouldn't stop you, because usually, it’s worth the refactor time. Now that you understand that organization is the key, you can help your future selves even further by making reusable components.


Why write a modal with the same animations, same design more than once, when it can become a reusable component. When you think about it, there are too many examples within your project where code could be extracted into a multi-use component. And worry not, VueJs is smart enough so it lets every separate component track its state.

Inspired as you should be, go wild and refactor!

2. Parent and child component communication

There are many patterns used to simplify communications. If the child component is right below its parent, meaning that there’s no deeper nesting and it is direct communication, Vue’s got you covered.
Imagine that our to-be-reusable component, Modal, looks like this

<template>
   <transition name="modal">
       <div @click="$emit('close-modal')">
           <div @click.stop >
               <p>{{ someTextFromParent }}</p>
           </div>
       </div>
   </transition>
</template>
<script>
   export default {
       props: ['some-text-from-parent']
   }
</script>

So, to pass some data from parent to child, you can include it in the list of props the child component accepts, using a props option. It can be an array with a list of names, or an object, which allows you to define types, default values, etc… Keep in mind that when sending property with “v-bind”, you write it how it’s common for HTML, kebab-case. But when you use that property in a child, you can call it with camelCase notation.

Modal is called from the parent like this:

<template>
   <div>
       <button @click="showModal = true">Click me to show modal!</button>
       <Modal
           v-if="showModal"
           @close-modal="showModal = false"
           :some-text-from-parent="someText"
       >
       </Modal>
   </div>
</template>
 
<script>
import Modal from './utilities/Modal.vue';
 
export default {
   components: {
       Modal,
   },
   data() {
       return {
           someText: '',
           showModal: false,
       };
   },
};
</script>

For an opposite way of communication, from a child to a parent, you just have to invent an event! Just like “@click” or “@keyup”, you can call your own event like this:

@event-name="callback"

The callback is called when the event is triggered. Pass a name of a method that will process this event. Or just write it inline if it’s short enough.

But where do you emit this event? The answer is, in the child component.
On the click event, or anywhere in code really, just write:

@click="$emit('event-name', data)"

And you are good to go. Data is not mandatory. In our example, you don’t need to send anything, but to let the parent know what has happened.

For more complicated communications, like from parent to another parent, or deeply nested children, there are other patterns people came up with. Some of them are Vuex and Event Bus. Vuex is practically a standard nowadays so we advise you to dive deep and investigate!

3. Two-way binding

A true two-way binding between properties of parent and child components is not something commonly done in Vue 2. A child component could mutate the parent, and the source of that mutation might not be obvious for both components.

Let’s jump on an example. Imagine that you have some kind of a form that is divided into multiple components, something like this in our parent, “Form” component: 

<template>
   <form @submit.prevent>
       <AddressRelatedFormPart :form-data.sync="formFields" />
       <BillingRelatedFormPart :form-data.sync="formFields" />
 
       <button @click="formFields.email = 'example@example.com'">Reset</button>
   </form>
</template>
 
<script>
import AddressRelatedFormPart from './AddressRelatedFormPart.vue';
import BillingRelatedFormPart from './BillingRelatedFormPart.vue';
 
export default {
   components: {
       AddressRelatedFormPart,
       BillingRelatedFormPart,
   },
   data() {
       return {
           formFields: {
               email: '',
               phone: '',
               cardNumber: '',
           },
       };
   },
};
</script>

So let’s say the parent is responsible for submitting the form if all form parts are valid. All fields’ data is kept here and then passed to children. Children can bind fields to corresponding input types, validate them, do whatever they need to do, however, you want your parent component to be informed of any field’s new state. 

To avoid any potential maintenance issue, Vue’s team made a .sync directive. This helps you synchronize “shared” data (since Vue 2.3.0).

So, the next step would be for “form part” components (AddressRelatedFormPart for example) to inform the parent of the change. It emits the “update” event, which .sync automatically listens to. There is no need to write a “v-on” listener! 

The emitting would be done this way:

export default {
   props: ['form-data'],
 
   data() {
       return {
           formDataObj: Object.assign({}, this.formData),
       };
   },
 
   methods: {
       handleEmit(data, field) {
           this.formDataObj[field] = data;
           this.$emit('update:formData', {
               ...this.formDataObj,
               [field]: data,
           });
       },
   },
};

But why is it important that you operate with a separate, local copy of a received object? The reason is that you want to allow your prop to change from within a child. As you probably know, you can’t exactly modify the prop, and that’s good.

So, you make an illusion that the prop is modified. What happens is that you are emitting a completely new value. Or regarding objects, a new reference. You receive it through prop, make a copy so that your child component can operate with it, and emit a new value so that the parent’s original data is synchronized

The HTML would be written like this: It calls your method “handleEmit” whenever the input event is triggered.

<template>
   <div>
       <input
           type="text"
           :value="formDataObj.email"
           @input="handleEmit($event.target.value, 'email')"
       />
       <input
           type="text"
           :value="formDataObj.phone"
           @input="handleEmit($event.target.value, 'phone')"
       />
   </div>
</template>

Another question arises. If this is closest to true two-way binding, how will the child be informed if now the original data changes from within a parent? If you thought that re-render will do its magic, you are in the wrong, pal! 

To solve this problem, you need to add a watcher in your child. The watcher will keep the eye open for any incoming change that parent might invoke. We set it to watch it deeply, because of how nested objects can be.

Your child component, in the end, looks like this:

<template>
   <div>
       <input
           type="text"
           :value="formDataObj.email"
           @input="handleEmit($event.target.value, 'email')"
       />
       <input
           type="text"
           :value="formDataObj.phone"
           @input="handleEmit($event.target.value, 'phone')"
       />
   </div>
</template>
<script>
export default {
   props: ['form-data'],
 
   data() {
       return {
           formDataObj: Object.assign({}, this.formData),
       };
   },
 
   methods: {
       handleEmit(data, field) {
           this.formDataObj[field] = data;
           this.$emit('update:formData', {
               ...this.formDataObj,
               [field]: data,
           });
       },
   },
 
   watch: {
       formData: {
           deep: true,
           handler(changedData) {
               this.formDataObj = Object.assign({}, changedData);
           },
       },
   },
};
</script>

And tada! The form data is fully synchronized!

4. Using .sync inside a v-for loop

When working with .sync, you might fall for a common trap, and God, we wish we knew this when we needed it first! If you find yourself using v-for in conjunction with .sync, you need to know about this trick.

Imagine that you decide to make a reusable “FormPart” component instead of having AddressRelatedFormPart & BillingRelatedFormPart. Now, your Form calls all form parts using v-for:

<form @submit.prevent>
           <FormPart
               v-for="(part, index) in formParts"
               :key="index"
               :form-data.sync="formParts[index]"
           />
           <button>Submit</button>
       </form>

Where “formParts” looks like this:

data() {
       return {
           formParts: [
               {
                   email: '',
                   phone: '',
               },
               {
                   cardNumber: '',
               },
           ],
       };
   },

By mere logic, you’d think there’s been a mistake. Why would you write

:form-data.sync="formParts[index]"

Instead of

:form-data.sync="part"

As even ESLint marks it as an error, saying “'.sync' modifiers cannot update the iteration variable 'part’. “, you can tell that this looks like a big problem.

The thing is: the “part”, a looping item, is not a reference to the actual element in the array (object in our case), and cannot be synced. So you might create something like this, and expect it to work. However, you can say that you are syncing “formParts[index]”, meaning the referenced element on that index in the array.

In the following example of how our “FormPart” component is written, you can notice that even “v-model” works by the same logic. It needs an actual reference, which you will only get by accessing an array with indices.

<template>
   <div>
       <input
           type="text"
           v-for="(data, field) in allFields"
           :key="field"
           v-model="allFields[field]"
           @input="handleEmit($event.target.value, field)"
       />
   </div>
</template>
 
<script>
export default {
   props: ['form-data'],
   data() {
       return {
           allFields: Object.assign({}, this.formData),
       };
   },
   methods: {
       handleEmit(data, field) {
           this.$emit('update:formData', {
               ...this.allFields,
               [field]: data,
           });
       },
   },
   watch: {
       formData: {
           deep: true,
           handler(changedObject) {
               this.allFields = Object.assign({}, changedObject);
           },
       },
   },
};
</script>

5. “data” must be a function

Why is it important that only data remains a function that must return an object? It’s very important because that is how Vue keeps every instance of this component to track their state, not to share the same one.

When returning a function, each instance can maintain an independent copy of the returned data object. 

Imagine that your data looked like this:

data: {
       formFields: {
           email: '',
           phone: '',
           cardNumber: '',
       },
   },

By doing this, you will let every instance of the “Form” component act like one. You’d write an email in one input field, and every other “Form” component with its email field would have it populated. Like a copy-paste of the same thing. That is definitely what you do not want to achieve.

Conclusion

If you made it this far, congratulations! That means that you are ready to learn and that alone will set you on the right track while trying to become a VueJs developer! Now it’s your turn to pass the knowledge to your colleagues.

You’ve learned how to plan your components and make them reusable, why data must be a function, how components communicate in various ways. There is still one important thing about component communication left uncovered, and it will be a piece of cake now that you understand .sync! More VueJS tips&trick coming soon in our next blog. Till then check out our other tech blogs and stay tuned folks! :)