Building applications with frontend frameworks and libraries is simple because you do not need to know all the advanced concepts of your preferred library or framework to get a basic application working. However, in building mid to large-scale applications, one of the essential concepts you need to know to kickstart your application is state management.
A frontend application built with Single Page Application (SPA) popular frameworks and libraries like React, Vue, Svelte, and Angular, follows a similar paradigm—components. Components are reusable sections of a SPA that enforce the “don’t repeat yourself” principle in SPAs. Each component either has its data or receives it as props through parent components. These props could either be static (immutable) or dynamic (mutable). The dynamic ones can change due to the user’s interactions with the app and are therefore stored in a state. A state is simply a data snapshot of your application at a given time. It’s the absolute representation of every form of data in your application at the moment.
The state can be passed as props from one parent component to one or more child components, not vice versa. In some cases, state data might need to be shared among sibling components and there’s no way props could be passed between each sibling component. For this scenario, a centralized store is introduced to the application. A centralized store facilitates access to data from any level in the application—parent, child, and sibling components, all have access to the store’s data.
For a simple application, with little frontend knowledge, the state store can be easily configured in the application. However, for a large-scale application state, you might need a state management library in your application. For the management of mid to large-scale applications, Facebook developed a state management architecture—the Flux architecture. Flux is a software design pattern that allows developers to build scalable and maintainable frontend applications through proper state management. After its release, several state management libraries like Redux and Vuex have been built following its pattern.
Vuex is a popular state management library specific to Vue applications. Redux is also a state management library used in React applications but unlike Vuex, with configurations, it can manage states in a Vue application.
In this article, the focus will be on Vuex. You’ll learn what Vuex is, the key concepts of the Vuex library, the installation process, and some advanced features of Vuex.
What is Vuex?
Vuex is a state management library used in Vue applications. It integrates a fluid experience into an existing application by providing a single source of truth—the state—which is accessible to every component in the app. Vuex follows the Flux architecture by using a unidirectional data flow, where the application’s state is kept in a centralized store and all changes through that state must occur through a set of predefined actions.
Apart from the issue of passing states among sibling components, in large applications, passing states from a parent component to a child component can be difficult when the child component is many levels far from the parent component. The process of passing props from one component to another to reach a deeply nested component is unsafe and can introduce bugs to your code. Vuex eliminates this prop-passing method by giving all components an equal level of access to the store.
Vuex is efficient and effective for mid to large-scale applications but can be lengthy for small-scale applications. In small-scale applications like a counter app, a static website with a few routes, or proof-of-concept apps, you do not need Vuex as there are only a few files in these apps and a local component state is feasible. An easy way to know if your application needs a state management library is to apply this quote from Dan Abramov, the author of Redux,
- Flux libraries are like glasses; you’ll know when you need them.
Core Concepts of Vuex
Vuex manages an application's state through a centralized store. The store has four core properties of Vuex—state, mutation, action, and getters—each with a responsibility. The state is the single source of truth responsible for the views passed and dispatched in components. Mutations are methods that directly mutate the state in a store. Actions call (or commit to) mutations, while Getters are to Vuex store what the computed property is to components.
States are the data used in the components. These states can be rendered in the components when they are needed and also be mutated directly in the store by mutations. In building the state of an application, it is essential to separate component-level states from application-level states. Component-level states are only useful within a single component and are not shared or passed as props from parent components to child components. Application-level states on the other hand are shared among many components. Adding a component-level state to the Vuex store makes the store unnecessarily bulky. Since the state is only useful in a single component and the component can manage the state without issues, it is redundant to place the state in a store and pass it down as a prop to the component.
Mutations are Vuex methods that directly mutate the state, as the name implies. They consist of a string type and a handler. The string types are declared in uppercase for ease of identification and linting/tooling purposes. Mutations have access to the Vuex store states. The state is passed as its first argument. An optional argument that depends on its use in Vuex Actions is the payload.
- As a best practice, you are expected to pass the payload as an object rather than any other data type.
Another best practice to note is to avoid executing asynchronous operations in the mutation method. Since Mutations are synchronous, they won’t wait till an asynchronous task is completed before modifying the state with readily available data. Also, asynchronous operations in Mutations make debugging difficult. When debugging a Vue app, the dev tool captures the “before” and “after” of every logged mutation and since the asynchronous callback fails to return before the mutation is committed, the dev tool becomes unaware of the response of the asynchronous callback function and never logs it. Mutations should therefore be kept free from asynchronous operations and other side effects.
Actions are in charge of committing Mutations. They receive the payload from the component as an optional second argument. The first argument in actions is the context and it allows actions to access every store property and commit them to mutations. As mentioned in the Mutations section, it is safe to run asynchronous operations in Actions because they wait for the callback function response before committing to Mutations.
Finally, Getters are used to derive computed information from the store state. They are similar to the single component’s computed property. Getters have direct access to the store state. They receive it as their first argument, hence their ability to run computation operations on the state properties.
All together, the four properties are stored in an object—the Vuex store. The store object gives Mutations and Getters access to the state and helps Actions commit directly to Mutations using context.commit
. Using createStore({})
, a method from the Vuex library, you can create a store for your project and import it into the components you want to use it or use it globally without importing by prefixing it in your component with $ as in this.$store.
Using Vuex in a Simple Counter App
To consolidate your knowledge of Vuex so far, you’ll walk through the build process of a simple counter app. Although a counter app is a small-scale application and does not need Vuex, the purpose of its use in this article is to equip you with firsthand working knowledge of the Vuex library.
Steps
- Create a Vue application
npm create vue@latest
- Install Vuex
npm install vuex@next --save
#(or)
yarn add vuex@next --save
- Create a Single-File Component (SFA)
<template>
<button @click="increment">Increment +</button>
<button @click="decrement">Increment +</button>
<h2>Count is: {{ count }}</h2>
</template>
<script>
export default {
data() {
return {
count: 0,
}
},
methods: {
increment() {
this.count++
},
decrement() {
this.count--
},
}
}
</script>
<style scoped> /* scoped - the style is limited to this file alone */
button {
font-weight: 500;
}
h2 {
font-size: 36px;
font-weight: 600;
}
</style>
- Create the store in a separate store.js file and register it in the main.js or app.js file—the entry js file for your Vue app.
import { createStore } from "vuex"
const store = createStore({
state() {
return {
count: 0;
}
},
mutations: {
//destructured the payload to get and the object property (number)
//directly
increment (state, { number }) {
if (number) state.count += number;
else state.count++
},
decrement(state, { number }) {
if (number) state.count -= number;
else state.count--
}
}
})
- Use the store properties in your SFA (it’s accessed globally in this sample code)
<template>
<button @click="increment">Increment +</button>
<button @click="decrement">Increment +</button>
<h2>Count is: {{ $store.count }}</h2>
</template>
<script>
export default {
data() {
return {
count: this.$store.state.count
}
},
methods: {
increment() {
// the increment Action is committed without a payload
this.$store.commit.("increment")
},
decrement() {
// the decrement Action is committed with a payload
this.$store.commit("decrement", { number: 2 })
},
}
}
</script>
<style scoped> /* scoped - the style is limited to this file alone */
button {
font-weight: 500;
}
h2 {
font-size: 36px;
font-weight: 600;
}
</style>
In this simple counter app, you used Vuex to manage the count state by committing the increment and decrement actions with or without a payload. When a payload argument is passed, either of the mutation methods that received it used it to update the state, and when it’s not passed, the state is updated by adding or removing 1. The Vuex State, Mutations, and Actions properties are used here but not Getters. In building your application, based on your requirements, you can use the Vuex Getters property in a similar scenario where the computed property will come in handy.
Advanced Features of Vuex
At this point, you have what it takes to integrate and use Vuex in your next project. At the start of the article, it was mentioned that you need only the basic concepts of Vuex to get started, however, it is important to familiarize yourself with some of Vuex's advanced powerful features.
Plugins: These are simply functions that receive the Vuex store as their only argument. They cannot mutate the state directly but only trigger changes by committing to mutations.
Strict mode: You can turn on strict mode by setting the property in the Vuex store. Strict mode ensures that the state is not mutated in any function other than mutations as it raises warnings to tell you when you do. It’s important to note that strict mode is useful only during development and should not be enabled in production.
Form-handling: Handling forms in a Vue application can be tricky when using v-model with a Vuex state. Since the directive will attempt to mutate the state directly, Vuex will continuously throw an error. Rather than using v-model in forms with Vuex, you can bind the form value to the component data property and commit changes to Vuex mutations on change by using the @change/@input directive.
Modules: Once you notice that your Vuex store is already expanding due to too many state object properties, you can create modules containing state objects in separate files and import them into the Vuex store. After importing them, you can pass them as a property of the Vuex store’s state object.
Vuex Best Practices
In using Vuex optimally in your applications, there are just a few best practices to keep up with, and some of them have been mentioned in the sections above already.
Asynchronous operations should never be performed in Mutations. They should only be run in actions before committing to mutations.
Any direct mutation to the state should be performed in Mutations alone and not in any other Vuex store property.
Irrespective of the complexity or scale of your application, keep your store simple.
Finally, use getters for computed properties.
Conclusion
In this article, you’ve learned about state and state management in a Vue app with Vuex specifically. You’ve covered the basic concepts of Vuex, seen a sample application of the Vue state management library, and been introduced to Vuex's advanced concepts and best practices for writing optimal Vue applications. It does not end there. Due to some issues with the Vuex library, Vue now has another official state management library—Pinia.
Pinia is the latest Vue state management library. It fixes the issues developers encounter in Vuex 3 and 4 and it’s also a simple library to use and get started on. In my next article, you’ll be introduced to Pinia, the bugs it fixed in Vuex, and how better it is compared to Vuex in terms of its methods and functions.
- NB: Vuex 3 and 4 still function and are still maintained by the Vue team, so, you can still use them in your application. Also, it’s important to know that Pinia is essentially Vuex 5 and your knowledge of Vuex can get you started easily.