As developers, we want to produce manageable and maintainable code, which is also easier to debug and test. To make this possible, we adopt best practices known as patterns. Patterns are proven algorithms and architectures, which help us to do particular tasks in an efficient and predictable way.
In this tutorial, we’ll look at the most common Vue.js component communication patterns, along with some pitfalls we should avoid. We all know that, in real life, there is no single solution to all problems. In the same way, in Vue.js app development, there is no universal pattern for all programming scenarios. Each pattern has its own advantages and drawbacks, and it’s suitable for particular use cases. The essential thing for Vue.js developers is to know all the most common patterns, so we can choose the right one for a given project. This will lead to proper and efficient component communication.
Why Proper Components Communication Is Important?
When we build an app with a component-based framework like Vue.js, we aim to make our app’s components as isolated as they can be. This makes them reusable, maintainable, and testable. To make a component reusable, we need to shape it in a more abstract and decoupled (or loosely coupled) form, and as such, we can add it to our app or remove it without breaking the app’s functionality.
However, we can’t achieve complete isolation and independence in our app’s components. At some point they need to communicate each other: to exchange some data, to change app’s state, etc. So, it’s important for us to learn how to accomplish this communication properly while still keeping the app working, flexible, and scalable.
Vue.js Components Communication Overview
In Vue.js, there are two main types of communication between components:
Direct parent-child communication, based on the strict parent-to-child and child-to-parent relationships.
Cross-components communication, in which one component can “talk” to any other one regardless their relationships.
In the following sections, we’ll explore both types along with appropriate examples.
Direct Parent-Child Communication
The standard model components communication, which Vue.js supports out of the box, is the parent-child model realized via props and custom events. In the diagram below, you can see a visual representation of how this model looks in action.
As you can see, a parent can communicate only with its direct children, and children can communicate directly only with their parent. In this model, no sibling or cross-component communication is possible.
In the following sections, we’ll take the components from the diagram above and will implement them in a series of practical examples.
Parent-to-Child Communication
Let’s suppose the components we have are part of a game. Most games display the game score somewhere in their interface. Imagine that we have a score variable declared in the Parent A component, and we want to display it in the Child A component. So, how can we do that?
To dispatch data from a parent to its children Vue.js uses props. There are three necessary steps to pass down a property:
Registering the property in the child, like this props: ["score"]
Using the registered property in the child’s template, like this Score: {{ score }}
Binding the property to the score variable (in parent’s template), like this
Let’s explore the full example to better understand what really happens:
// HTML part
// JavaScript part
Vue.component('ChildB',{
template:`
Child B
data {{ this.$data }}
`,
})
Vue.component('ChildA',{
template:`
Child A
data {{ this.$data }}
Score: {{ score }} // 2.Using
`,
props: ["score"] // 1.Registering
})
Vue.component('ParentB',{
template:`
Parent B
data {{ this.$data }}
`,
})
Vue.component('ParentA',{
template:`
Parent A
data {{ this.$data }}
// 3.Binding
`,
data() {
return {
score: 100
}
}
})
Vue.component('GrandParent',{
template:`
Grand Parent
data {{ this.$data }}
`,
})
new Vue ({
el: '#app'
})
CodePen Example
Validating Props
For brevity and clarity, I registered the props by using their shorthand variant. But in real development it’s recommended to validate the props. This will assure that the props will receive the correct type of value. For example, our score property could be validated like this:
props: {
// Simple type validation
score: Number,
// or Complex type validation
score: {
type: Number,
default: 100,
required: true
}
}
When using props, please make sure you understand the difference between their literal and dynamic variants. A prop is dynamic when we bind it to some variable (for example, v-bind:score="score" or its shorthand :score="score"), and thus, the prop’s value will vary depending on the variable’s value. If we just put some value without the binding, then that value will be interpreted literally and the result will be static. In our case, if we write it score="score", it would display score instead of 100. This is a literal prop. You should be careful of that subtle difference.
Updating a Child Prop
So far, we have successfully displayed the game score, but at some point we’ll need to update it. Let’s try this.
We created a changeScore() method, which should update the score after we press the Change Score button. When we do so, it seems that the score is updated properly, but we get the following Vue warning in the console:
[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop’s value. Prop being mutated: “score”
As you can see, Vue tells us that the prop will be overwritten if the parent re-renders. Let’s test this by simulating such behavior with the built-in $forceUpdate() method:
Now, when we change the score and then press the Rerender Parent button, we can see that the score go back to its initial value from the parent. So Vue is telling the truth!
Keep in mind though that arrays and objects will affect their parents, because they are not copied, but passed by reference.
So, when we need to mutate a prop in the child, there are two ways to workaround this re-render side effect.
Mutating a Prop With a Local Data Property
The first method is to turn the score prop into a local data property (localScore), which to use in the changeScore() method and in the template:
Here, we created computed doubleScore(),which multiplies the parent’s score by two and then, the result is displayed in the template. Obviously, pressing the Rerender Parent button won’t have any side effect.
Child-to-Parent Communication
Now, let’s see how components can communicate the opposite way.
We’ve just seen how to mutate a prop in the child, but what if we need to use that prop in more than one child component. In that case, we’ll need to mutate the prop from its source in the parent, so all the components which use the prop will be updated correctly. To satisfy this requirement, Vue introduces custom events.
The principle here is that we notify the parent for the change we want to do, the parent does that change, and that change is reflected via the passed prop. Here are the necessary steps for this operation:
In the child, we emit an event describing the change we want to perform, like this this.$emit('updatingScore', 200)
In the parent, we register an event listener for the emitted event, like this @updatingScore="updateScore"
When the event is emitted the assigned method will update the prop, like this this.score = newValue
Let’s explore the full example to better understand how this happens:
We use the built-in $emit() method to emit an event. The method takes two arguments. The first argument is the event we want to emit, and the second is the new value.
The .sync Modifier
Vue offers a .sync modifier which works similarly and we may want to use it as a shortcut in some cases. In such a case, we use the $emit() method in a slightly different way. As the event argument we put update:score like this this.$emit('update:score', 200), and then, when we bind the score prop, we add the .sync modifier like this . In the Parent A component, we remove the updateScore() method and the event registration (@updatingScore="updateScore") as they are not needed anymore.
Why Not Use this.$parent and this.$children for Direct Parent-Child Communication?
Vue offers two API methods which give us direct access to parent and child components: this.$parent and this.$children. At first it may be tempting to use them as a quicker and easier alternative to props and events, but we should not. This is considered a bad practice, or anti-pattern, because it forms tight coupling between parent and child components. The latter leads to inflexible and easy to break components, which are hard to debug and reason about. These API methods are rarely used, and as a rule of thumb, we should avoid them or use them with caution.
Two-Way Component Communication
Props and events are unidirectional. Props goes down, events goes up. But by using props and events together, we can effectively communicate up and down the component tree, resulting in two-way data binding. This is actually what the v-model directive does internally.
Cross-Components Communication
The parent-child communication pattern quickly becomes inconvenient and impractical as our app’s complexity grows. The problem with the props-events system is that it works directly, and it is tightly bound to the component tree. Vue events don’t bubble, in contrast to native ones, and that’s why we need to repeat emitting them up until we reach the target. As a result our code becomes bloated with too many event listeners and emitters. So, in more complex applications, we should consider using a cross-components communication pattern.
Let’s take a look at the diagram below:
As we can see, in this any-to-any type of communication, each component can send and/or receive data from any other component without need of intermediate steps and intermediary components.
In the following sections, we’ll explore the most common implementations of cross-components communication.
Global Event Bus
A global event bus is a Vue instance, which we use to emit and listen for events. Let’s see it in practice.
Here are the steps to create and use an event bus:
Declaring our event bus as a new Vue instance, like this const eventBus = new Vue ()
Emitting an event from the source component, like this eventBus.$emit('updatingScore', 200)
Listening for the emitted event in the target component, like this eventBus.$on('updatingScore', this.updateScore)
In the above code example, we remove @updatingScore="updateScore" from the child, and we use the created() lifecycle hook instead, to listen for the updatingScore event. When the event is emitted, the updateScore() method will be executed. We can also pass the updating method as an anonymous function:
created () {
eventBus.$on('updatingScore', newValue => {this.score = newValue})
}
Global event bus pattern can solve the problem with event bloat to some extend, but it introduces some other issues. The app’s data can be changed from any part of the app without leaving traces. This makes the app harder to debug and test. For more complex apps, where the things can quickly get out of control, we should consider a dedicated state management pattern, such as Vuex, which will give us more fine-grained control, better code structure and organization, and useful change tracking and debugging features.
Vuex
Vuex is a state management library tailored for building complex and scalable Vue.js applications. The code written with Vuex is more verbose, but this can pay off in the long run. It uses a centralized store for all the components in an application, making our apps more organized, transparent, and easy to track and debug. The store is fully reactive, so the changes we make are reflected instantly.
Here, I’ll give you a brief explanation of what Vuex is, plus a contextual example. If you want to dive deeper into Vuex I suggest you to take a look at my dedicated tutorial about building complex applications with Vuex.
Let’s now explore the following diagram:
As you can see, a Vuex app is made of four distinguish parts:
State is where we hold our application data.
Getters are methods to access the store state and render it to the components.
Mutations are the actual and only methods allowed to mutate the state.
Actions are methods for executing asynchronous code and trigger mutations.
Let’s create a simple store and see how all this works in action.
An incrementScore() mutation, which will increment the score with a given value.
A score() getter, which will access the score variable from the state and will render it in components.
An incrementScoreAsync() action, which will use the incrementScore() mutation to increment the score after a given period of time.
In the Vue instance, instead of props we use computed properties to get the score value via getters. Then, to change the score, in the Child A component we use the mutation store.commit('incrementScore', 100). In the Parent B component we use the action store.dispatch('incrementScoreAsync', 3000).
Dependency Injection
Before we wrap up, let’s explore one more pattern. Its use cases are mainly for shared component libraries and plugins, but it’s worth mention it for completeness.
Dependency injection allows us to define a service via provide property, which should be an object or a function that returns an object, and make it available to all of component’s descendants, not just its direct children. Then, we can consume that service via inject property.
By using the provide option in Grand Parent component, we made the score variable available to all of its descendants. Each one of them cean gain access to it by declaring inject: ['score'] property. And, as you can see, the score is displayed in all components.
Note: The bindings which dependency injection creates are not reactive. So, if we want the changes made in the provider component to be reflected in its descendants, we have to assign an object to a data property and use that object in the provided service.
Why not Us this.$root for Cross-Components Communication?
The reasons we should not use this.$root are similar to those for this.$parent and this.$children described before—it creates too many dependencies beterrn. Relying on any of these methods for component communication must be avoided.
How to Choose the Right Pattern?
So, you already know all common ways of component communication. But how to decide which one fits best for your scenario?
Choosing the right pattern depends on the project you’re involved in or the application you want to build. It depends on complexity and type of your application. Let’s explore the most common scenarios:
In simple apps the props and events will be all you need.
Middle-range apps will require more flexible ways of communication, such as event bus and dependency injection.
For complex, large-scale apps you will definitely need the power of Vuex as a full-featured state management system.
And one last thing. You are not required to use any of the explored patterns only because someone else tells you to do so. You are free to choose and use whatever pattern you want, as long as you manage to keep your app working, and easy to maintain and scale.
Conclusion
In this tutorial, we learned the most common Vue.js components communication patterns. We saw how to implement them in practice and how to choose the right one, which fits best for our project. This will ensure that the app we have built use the proper type of components communication which makes it fully working, maintainable, testable and scalable.
This website uses cookies to improve your experience. AcceptRead More
Privacy & Cookies Policy
Privacy Overview
This website uses cookies to improve your experience while you navigate through the website. Out of these, the cookies that are categorized as necessary are stored on your browser as they are essential for the working of basic functionalities of the website. We also use third-party cookies that help us analyze and understand how you use this website. These cookies will be stored in your browser only with your consent. You also have the option to opt-out of these cookies. But opting out of some of these cookies may affect your browsing experience.
Necessary cookies are absolutely essential for the website to function properly. This category only includes cookies that ensures basic functionalities and security features of the website. These cookies do not store any personal information.
Any cookies that may not be particularly necessary for the website to function and is used specifically to collect user personal data via analytics, ads, other embedded contents are termed as non-necessary cookies. It is mandatory to procure user consent prior to running these cookies on your website.