How to set up communication between Vue.js components?

How to set up communication between Vue.js components?

Vue.js is a JavaScript framework allowing developers to create complex and dynamic web applications by assembling custom-made components in a virtual Document Object Model. But how can these components communicate and share data with each other?

Direct relative

First of all, let's consider this basic case: you have 2 kinds of components, a Parent, who has a given amount of money, and a Child, who can buy articles with the Parent's money.

schema-main.png
Basic example of communication

Now imagine that this parent has 2 children. To know whether he can afford an article with the parent's money, a child has to know the amount of money his parent can provide. In other words, the parent has to indicate to each of his children the amount made available for the purchase of items. We can represent this information by passing a prop moneyAvailable to each child.

Then, according to this amount, the child can buy items for a given price. Whenever he does, he has to tell his parent the amount spent, so the amount available for every other child is updated. To do so, we can create an event, purchaseItem, indicating to the parent the money spent. That is the most common case of communication between components that are directly related: when we go down the tree, we use a prop and when we wind it up, we use an event.

parent-1.png
Parent.vue
child-1.png
Child.vue

2 ways bindings with v-model

For a direct relationship between two components like this, we could use something similar to what's called 2-way binding in Angular by using the v-model directive :

vmodel-parent.png
Parent.vue - v-model
vmodel-child.png
Child.vue - v-model

Note: we can also use getter/setter methods in the child component to be more precise on the control of the data being forwarded, by adding processes before the return statement in the getter and before the emits in the setter :

vmodel-getter-setter.png
Child.vue - v-model with getter and setter

Indirect relative - bubbling effect

What if we add intermediaries between the children and parents? Let's say now we still have a child and a parent, but we add a sibling who has to approve every purchase. For the moment, the sibling only repeats to the child the money available and tells the parent the amount spent by the child.

sibling.png
Example with an intermediary

As we can see in the code, the sibling receives the money available as a prop and forwards it to the child. Then it also handles the purchase event of the child and emits a new one to the parent without adding any value.

For a single level, it looks acceptable. However, we can easily imagine how hard it would be if there were several more layers. Each one would have to forward the prop and the events of its own children components.

Let's have a look at the different available solutions.

Bus Event (Vuejs 2) - External Lib (mitt)

If you're still using Vue 2, there was a common practice that consisted of simulating an event bus by using a second Vue app. You can find more details on how it works in this article.

However, as mentioned in this RFC, it is no longer usable in a Vue 3 application. It is suggested to use a tierce library instead, such as mitt to have similar behavior.

Store

Let's consider the following application.

store-diagramme.png
Architecture diagramme

How can ChildD share some changes with ChildA ? Do we have to emit and trace an event back to the App component? If the Child B and C use data bound to the App, we'll have to add some code inside this component to ensure both children have the latest version of the data. With 2 components listening to a parent, it's achievable. However, if the application grows, bringing new components, and new relationships/dependencies between them, it probably won't be anymore.

When it comes to sharing data and synchronizing components' states, stores are never far away. Even if Pinia is now the official recommendation for Vue 3, we have to mention that there were 4 versions of VueX before that (and Pinia is actually considered as VueX 5). Broadly speaking, both of them work the same way: they manage stores that have a state which can be represented by an object, actions to perform changes on it, and getters to read it and make sure components are reactive to state changes. Components only have access to the last two, so they don't directly interact with the state.

Therefore, stores are useful if you want to share data across components that are not directly related, or not on the same branch of the DOM. To return to the previous example, where the App component manages a state with 2 components subscribed to it, we can place this state into the store, and create an action to initialize it. This action can perform an HTTP request, set an arbitrary random value to the state or whatever you want, but it will allow any component to initialize/modify/reset this state from anywhere and also make sure that every component that has a getter (representing the dependency between the component and this state) is being updated in real-time. For instance, if our parent has an amount of money placed in the state of a store and several intermediaries who each also have several children under them, every spending component can use a getter on the state money and trigger an action purchase which computes and update the new amount of money available for everybody.

Composables

Since the release of the Composition API, developers can now create variables and actions totally outside of any Vue component (or even a Vue app actually). Those variables are sharable between components and more importantly, they are reactive, meaning any change on the state will make the component using it re-render.
On top of that, the declaration and the use inside a component are really simple :

composable.png
Implementation of UseCounter
composable-use.png
Use of UseCounter

It does sound a lot like a VueX/Pinia store with the reactive states, the actions/mutations exposed and the reactivity provided for the components. It might be tempting to replace a state management library with a custom-made composable. However, it is important to understand that we should not confuse features and states management. Pinia and VueX are supposed, as the name suggests, to manage shared-states, preferably business-related, throughout the entire application. That has always been their main responsibility and even if they come with actions (and mutation for VueX) to manage their states, they cannot exist without them.

On the other hand, Composables have been designed to help developers to share functionnalities across their application. Some of them might have an internal state but that's not mandatory at all. We can take a look for instance at the VueUse library. It's a large toolbox allowing developers to compose their components with the functionalities they want, without reinventing the wheel. Some of them do not have or expose a state.

Lastly, I'd say that it is preferable to use Pinia (or VueX if you want) if you have to handle state management in your application because you'll find a lot of documentation, best practices and pieces of advice, alongside plugins, debugging tools like Vue Devtools, or even built-in features like modularization to help you.