Unlimited WordPress themes, graphics, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Code
  2. Vue
Code

Design Patterns for Communication Between Vue.js Components

by
Difficulty:AdvancedLength:LongLanguages:

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 Is Proper Component Communication 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 with each other: to exchange some data, to change the 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 Component Communication Overview 

In Vue.js, there are two main types of communication between components: 

  1. Direct parent-child communication, based on strict parent-to-child and child-to-parent relationships. 
  2. Cross-component communication, in which one component can "talk" to any other one regardless of their relationship. 

In the following sections, we'll explore both types, along with appropriate examples. 

Direct Parent-Child Communication

The standard model of component 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.

Parent-Child Component Communication diagram

As you can see, a parent can communicate only with its direct children, and children can only communicate directly 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 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:

  1. Registering the property in the child, like this: props: ["score"]
  2. Using the registered property in the child's template, like this: <span>Score: {{ score }}</span>
  3. Binding the property to the score variable (in parent's template), like this: <child-a :score="score"/>

Let's explore a full example to better understand what really happens:

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 ensure that the props will receive the correct type of value. For example, our score property could be validated like this:

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 a 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 in a value without the binding, then that value will be interpreted literally, and the result will be static. In our case, if we write 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:

CodePen Example

Now, when we change the score and then press the Rerender Parent button, we can see that the score goes 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 work around 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 we can use in the changeScore() method and in the template:

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:

CodePen Example

Here, we created a 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:

  1. In the child, we emit an event describing the change we want to perform, like this: this.$emit('updatingScore', 200)
  2. In the parent, we register an event listener for the emitted event, like this: @updatingScore="updateScore"
  3. When the event is emitted the assigned method will update the prop, like this: this.score = newValue 

Let's explore a full example to better understand how this happens:

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 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). Then, when we bind the score prop, we add the .sync modifier like this: <child-a :score.sync="score"/>. In the Parent A component, we remove the updateScore() method and the event registration (@updatingScore="updateScore") as they are not needed anymore.

CodePen Example

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 go down, events go 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-Component 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 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-component communication pattern. 

Let's take a look at the diagram below:

Cross-Component Communication diagram

As you can see, in this any-to-any type of communication, each component can send and/or receive data from any other component without the need for intermediate steps and intermediary components.

In the following sections, we'll explore the most common implementations of cross-component 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.

CodePen Example

Here are the steps to create and use an event bus:

  1. Declaring our event bus as a new Vue instance, like this: const eventBus = new Vue ()
  2. Emitting an event from the source component, like this: eventBus.$emit('updatingScore', 200)
  3. 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:

A global event bus pattern can solve the problem with event bloat to some extent, but it introduces 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 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 take a look at my dedicated tutorial about building complex applications with Vuex.

Let's now explore the following diagram:

Vuex Component Communication diagram

As you can see, a Vuex app is made of four distinct 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.

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 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 mentioning it for completeness.

Dependency injection allows us to define a service via the provide property, which should be an object or a function that returns an object, and make it available to all of the component's descendants, not just its direct children. Then, we can consume that service via the inject property.

Let's see this in action:

CodePen Example

By using the provide option in the Grand Parent component, we made the score variable available to all of its descendants. Each one of them can gain access to it by declaring the 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 Use this.$root for Cross-Component 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. Relying on any of these methods for component communication must be avoided.

How to Choose the Right Pattern

So you already know all the common methods of component communication. But how can you decide which one best fits your scenario?

Choosing the right pattern depends on the project you're involved in or the application you want to build. It depends on the 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 component 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 uses the proper type of component communication which makes it fully working, maintainable, testable, and scalable. 

Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.