State management is at the core of any frontend framework. There are several options when it comes to VueJS. This article will explore these patterns helping you to choose the right one depending on the use case. Before we look at state management patterns, it is essential to understand why state management is needed in the first place. Let’s find out.
Why do we need state management?
A VueJS application comprises many VueJS components. Most of these components are associated with data derived from the data() function. Let's have a look at an example VueJS component.
This simple component's state is associated with a data array of movies while the data belongs to the scope of this component. So far, so good. However, let's say that we need to access this data from a different component. Can we do that? Well, not if the data is local to this particular component. Here we have to look at state management beyond the component scope at the application level.
Why not props?
Like any other reactive application, VueJS shares data between multiple components through the use of props. You can pass data from a parent component to its child component using props. However, props are unidirectional. We can pass props from parent to child to grandchild and so on, but not the other way around. If you need to pass data from child to parent, you need to use Vue custom events. If you are unaware how this is done, refer to the VueJS documentation for more information. You will need this knowledge to understand the latter sections of this article.
Going beyond props
Now comes the question, “How do we pass data if two components aren’t directly related to each other, for example, sibling components?”
Let's look at three ways we can manage application-wide state in VueJS applications.
The three methods we'll be looking at are,
Using a global EventBus.
Using a simple global store.
Using the Vuex library.
1. Using an EventBus
Usually, components that don’t have a direct relationship cannot listen to custom events fired from each other. Therefore, they cannot pass data between them. However, this can be achieved by an EventBus. An EventBus follows a publisher/subscriber pattern. If the isolated components know the EventBus, they can subscribe to specific events. A publisher can publish events to the EventBus, and all subscribers of that event will get notified when the event is published.
With this mechanism, components can be decoupled and still react to events fired by each other. Let’s look at an example of EventBus communication in VueJS
import Vue from 'vue';
const eventBus = new Vue();
export default eventBus;
As of VueJS version 3.0, the Vue documentation recommends using an external library to implement the event emitter interface for the EventBus. One such external library is the mitt event emitter library.
As you can see, EventBus solves cross-component state management by helping to communicate data between components. However, there are few things to note. Using EventBus has a few disadvantages.
There is no centralized state management mechanism. It leads to data inconsistencies in large applications.
As the application grows, it'll be much harder to track state changes as we can't keep track of how the state changed or which components reacted to it.
Now, let's look at the second method to communicate application data and manage its state.
2. Using a simple global store
One of the main disadvantages of the EventBus was the lack of centralized state management. This issue gets solved if we introduce a global store every component can access. In this method, we try to place our data in a global store instead of within single components. The methods that mutate the data within the store will also be placed in the store itself.
The isolated components will talk to each other through the store. However, they still don’t directly communicate with each other. Therefore the components are loosely coupled. The store method “addMovies” will directly mutate the state. This is also known as store mutation. If one component wants to update the data, it should immediately act on the store method “addMovies.” The data displaying component will only think about getting the store value. It does not care about the store method.
Using a simple global store makes your state and component updates more manageable than the EventBus.
3. Using Vuex state management library
Vuex is a state management library built for the sole purpose of managing a centralized store for application state in VueJS. This is very similar to the Flux pattern developed by Facebook. The main part of Vuex is the Vuex Store. The Vuex store consists of 4 objects.
State - Contains properties that need to be shared with the entire application (application-wide data). It is a global singleton object, and the state is reactive. Therefore we shouldn't change the object directly.
Getters - Responsible for performing calculations or manipulations to store state before having that information accessible to components.
Actions - Responsible for calling mutations can be used to perform all asynchronous/complex logic before calling mutations. Here, the Actions are invoked using the “store.dispatch()” function.
Mutations - Functions responsible for directly mutating the state (As a common practice, mutations are named in capital letters. e.g., ADD_MOVIE). To invoke a mutation handler, the “store.commit()” function needs to be called. Mutations must always be synchronous.
As you can see, Vuex is an advanced extension of the simple global store we defined in the previous method. Further, Vuex can be integrated with VueJS dev tools that allow time travel debugging.
Vuex solves most problems that we faced with the previous methods. Some of them are as follows.
A centralized store that leads to consistent, shareable data across any component.
Adds structure on how state changes
It simplifies monitoring
Enables time-travel debugging and performance benchmarking
Ensures that you can mutate the state in a predictable manner
Gives the code more structure and maintainability
Allows dividing the store into modules with state, actions, getters, mutations to each store in the modules, useful for application scalability.
I've already discussed three state management patterns for VueJS applications. The choice of state management pattern depends on the scale of your application. For example, if your application is small and doesn't change frequently, you can use an EventBus with a stateless event-driven workflow. However, if your VueJS application is already large and growing, the most stable and efficient state management method would be to use the Vuex library, a centralized state management pattern.
Let me know your thoughts too about these patterns and how relevant they are for your use case.