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 itscore="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.
Vue.component('ChildA',{ template:`Child A
data {{ this.$data }}
Score: {{ score }}`,
props: ["score"],
methods: {
changeScore() {
this.score = 200;
}
}
})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:Vue.component('ParentA',{ template:`Parent A
data {{ this.$data }}
`,
data() {
return {
score: 100
}
},
methods: {
reRender() {
this.$forceUpdate();
}
}
})CodePen Example
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 thechangeScore()
method and in the template:Vue.component('ChildA',{ template:`Child A
data {{ this.$data }}
Score: {{ localScore }}`,
props: ["score"],
data() {
return {
localScore: this.score
}
},
methods: {
changeScore() {
this.localScore = 200;
}
}
})CodePen Example
Now, if we press the Rerender Parent button again, after we changed the score, we’ll see that this time the score remains the same.
Mutating a Prop With a Computed Property
The second method is to use the
score
prop in a computed property, where it will be transformed into a new value:Vue.component('ChildA',{ template:`Child A
data {{ this.$data }}
Score: {{ doubleScore }}
`,
props: ["score"],
computed: {
doubleScore() {
return this.score * 2
}
}
})CodePen Example
Here, we created computed
doubleScore()
,which multiplies the parent’sscore
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:
Vue.component('ChildA',{ template:`Child A
data {{ this.$data }}
Score: {{ score }}`,
props: ["score"],
methods: {
changeScore() {
this.$emit('updatingScore', 200) // 1. Emitting
}
}
})...
Vue.component('ParentA',{
template:`Parent A
data {{ this.$data }}
// 2.Registering
`,
data() {
return {
score: 100
}
},
methods: {
reRender() {
this.$forceUpdate()
},
updateScore(newValue) {
this.score = newValue // 3.Updating
}
}
})CodePen Example
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
ModifierVue 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 putupdate:score
like thisthis.$emit('update:score', 200)
, and then, when we bind thescore
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.Vue.component('ChildA',{ template:`Child A
data {{ this.$data }}
Score: {{ score }}`,
props: ["score"],
methods: {
changeScore() {
this.$emit('update:score', 200)
}
}
})...
Vue.component('ParentA',{
template:`Parent A
data {{ this.$data }}
`,
data() {
return {
score: 100
}
},
methods: {
reRender() {
this.$forceUpdate()
}
}
})CodePen Example
Why Not Use
this.$parent
andthis.$children
for Direct Parent-Child Communication?Vue offers two API methods which give us direct access to parent and child components:
this.$parent
andthis.$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.
const eventBus = new Vue () // 1.Declaring ... Vue.component('ChildA',{ template:`Child A
data {{ this.$data }}
Score: {{ score }}`,
props: ["score"],
methods: {
changeScore() {
eventBus.$emit('updatingScore', 200) // 2.Emitting
}
}
})...
Vue.component('ParentA',{
template:`Parent A
data {{ this.$data }}
`,
data() {
return {
score: 100
}
},
created () {
eventBus.$on('updatingScore', this.updateScore) // 3.Listening
},
methods: {
reRender() {
this.$forceUpdate()
},
updateScore(newValue) {
this.score = newValue
}
}
})CodePen Example
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 thecreated()
lifecycle hook instead, to listen for theupdatingScore
event. When the event is emitted, theupdateScore()
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.
const store = new Vuex.Store({ state: { score: 100 }, mutations: { incrementScore (state, payload) { state.score += payload } }, getters: { score (state){ return state.score } }, actions: { incrementScoreAsync: ({commit}, payload) => { setTimeout(() => { commit('incrementScore', 100) }, payload) } } }) Vue.component('ChildB',{ template:`Child B
data {{ this.$data }}`,
})Vue.component('ChildA',{
template:`Child A
data {{ this.$data }}
Score: {{ score }}`,
computed: {
score () {
return store.getters.score;
}
},
methods: {
changeScore (){
store.commit('incrementScore', 100)
}
}
})Vue.component('ParentB',{
template:`Parent B
data {{ this.$data }}
Score: {{ score }}`,
computed: {
score () {
return store.getters.score;
}
},
methods: {
changeScore (){
store.dispatch('incrementScoreAsync', 3000);
}
}
})Vue.component('ParentA',{
template:`Parent A
data {{ this.$data }}
`,
})Vue.component('GrandParent',{
template:`Grand Parent
data {{ this.$data }}
`,
})new Vue ({
el: '#app',
})CodePen Example
In the store, we have the following:
- A
score
variable set in the state object. - An
incrementScore()
mutation, which will increment the score with a given value. - A
score()
getter, which will access thescore
variable from the state and will render it in components. - An
incrementScoreAsync()
action, which will use theincrementScore()
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.
Let’s see this in action:
Vue.component('ChildB',{ template:`Child B
data {{ this.$data }}
Score: {{ score }}
`,
inject: ['score']
})Vue.component('ChildA',{
template:`Child A
data {{ this.$data }}
Score: {{ score }}
`,
inject: ['score'],
})Vue.component('ParentB',{
template:`Parent B
data {{ this.$data }}
Score: {{ score }}
`,
inject: ['score']
})Vue.component('ParentA',{
template:`Parent A
data {{ this.$data }}
Score: {{ score }}
`,
inject: ['score'],
methods: {
reRender() {
this.$forceUpdate()
}
}
})Vue.component('GrandParent',{
template:`Grand Parent
data {{ this.$data }}
`,
provide: function () {
return {
score: 100
}
}
})new Vue ({
el: '#app',
})CodePen Example
By using the
provide
option in Grand Parent component, we made thescore
variable available to all of its descendants. Each one of them cean gain access to it by declaringinject: ['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 forthis.$parent
andthis.$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.
Powered by WPeMatico