In single-page applications, the concept of state relates to any piece of data that can change. An example of state could be the details of a logged-in user, or data fetched from an API.
Handling state in single-page apps can be a tricky process. As an application gets larger and more complex, you start to encounter situations where a given piece of state needs to be used in multiple components, or you find yourself passing state through components that don’t need it, just to get it to where it needs to be. This is also known as “prop drilling”, and can lead to some unwieldy code.
Vuex is the official state management solution for Vue. It works by having a central store for shared state, and providing methods to allow any component in your application to access that state. In essence, Vuex ensures your views remain consistent with your application data, regardless of which function triggers a change to your application data.
In this article, I’ll offer you a high-level overview of Vuex and demonstrate how to implement it into a simple app.
Want to learn Vue.js from the ground up? Get an entire collection of Vue books covering fundamentals, projects, tips and tools & more with SitePoint Premium. Join now for just $9/month.
A Shopping Cart Example
Let’s consider a real-world example to demonstrate the problem that Vuex solves.
When you go to a shopping site, you’ll usually have a list of products. Each product has an Add to Cart button and sometimes an Items Remaining label indicating the current stock or the maximum number of items you can order for the specified product. Each time a product is purchased, the current stock of that product is reduced. When this happens, the Items Remaining label should update with the correct figure. When the product’s stock level reaches 0, the label should read Out of Stock. In addition, the Add to Cart button should be disabled or hidden to ensure customers can’t order products that are currently not in inventory.
Now ask yourself how you’d implement this logic. It may be trickier than you think. And let me throw in a curve ball. You’ll need another function for updating stock records when new stock comes in. When the depleted product’s stock is updated, both the Items Remaining label and the Add to Cart button should be updated instantly to reflect the new state of the stock.
Depending on your programming prowess, your solution may start to look a bit like spaghetti. Now, let’s imagine your boss tells you to develop an API that allows third-party sites to sell the products directly from the warehouse. The API needs to ensure that the main shopping website remains in sync with the products’ stock levels. At this point you feel like pulling your hair out and demanding why you weren’t told to implement this earlier. You feel like all your hard work has gone to waste, as you’ll need to completely rework your code to cope with this new requirement.
This is where a state management pattern library can save you from such headaches. It will help you organize the code that handles your front-end data in a way that makes adding new requirements a breeze.
Prerequisites
Before we start, I’ll assume that you:
You’ll also need to have a recent version of Node.js that’s not older than version 6.0. At the time of writing, Node.js v10.13.0 (LTS) and npm version 6.4.1 are the most recent. If you don’t have a suitable version of Node installed on your system already, I recommend using a version manager.
Finally, you should have the most recent version of the Vue CLI installed:
npm install -g @vue/cli
Build a Counter Using Local State
In this section, we’re going to build a simple counter that keeps track of its state locally. Once we’re done, I’ll go over the fundamental concepts of Vuex, before looking at how to rewrite the counter app to use Vue’s official state management solution.
Getting Set Up
Let’s generate a new project using the CLI:
vue create vuex-counter
A wizard will open up to guide you through the project creation. Select Manually select features and ensure that you choose to install Vuex.
Next, change into the new directory and in the src/components
folder, rename HelloWorld.vue
to Counter.vue
:
cd vuex-counter
mv src/components/HelloWorld.vue src/components/Counter.vue
Finally, open up src/App.vue
and replace the existing code with the following:
<template>
<div id="app">
<h1>Vuex Counter</h1>
<Counter/>
</div>
</template>
<script>
import Counter from './components/Counter.vue'
export default {
name: 'app',
components: {
Counter
}
}
</script>
You can leave the styles as they are.
Creating the Counter
Let’s start off by initializing a count and outputting it to the page. We’ll also inform the user whether the count is currently even or odd. Open up src/components/Counter.vue
and replace the code with the following:
<template>
<div>
<p>Clicked {{ count }} times! Count is {{ parity }}.</p>
</div>
</template>
<script>
export default {
name: 'Counter',
data: function() {
return {
count: 0
};
},
computed: {
parity: function() {
return this.count % 2 === 0 ? 'even' : 'odd';
}
}
}
</script>
As you can see, we have one state variable called count
and a computed function called parity
which returns the string even
or odd
depending on the whether count
is an odd or even number.
To see what we’ve got so far, start the app from within the root folder by running npm run serve
and navigate to http://localhost:8080.
Feel free to change the value of the counter to show that the correct output for both counter
and parity
is displayed. When you’re satisfied, make sure to reset it back to 0 before we proceed to the next step.
Incrementing and Decrementing
Right after the computed
property in the <script>
section of Counter.vue
, add this code:
methods: {
increment: function () {
this.count++;
},
decrement: function () {
this.count--;
},
incrementIfOdd: function () {
if (this.parity === 'odd') {
this.increment();
}
},
incrementAsync: function () {
setTimeout(() => {
this.increment()
}, 1000)
}
}
The first two functions, increment
and decrement
, are hopefully self-explanatory. The incrementIfOdd
function only executes if the value of count
is an odd number, whereas incrementAsync
is an asynchronous function that performs an increment after one second.
In order to access these new methods from the template, we’ll need to define some buttons. Insert the following after the template code which outputs the count and parity:
<button @click="increment" variant="success">Increment</button>
<button @click="decrement" variant="danger">Decrement</button>
<button @click="incrementIfOdd" variant="info">Increment if Odd</button>
<button @click="incrementAsync" variant="warning">Increment Async</button>
After you’ve saved, the browser should refresh automatically. Click all of the buttons to ensure everything is working as expected. This is what you should have ended up with:
See the Pen Vue Counter Using Local State by SitePoint (@SitePoint) on CodePen.
The counter example is now complete. Let’s move and examine the fundamentals of Vuex, before looking at how we would rewrite the counter to implement them.
How Vuex Works
Before we go over the practical implementation, it’s best that we acquire a basic grasp of how Vuex code is organized. If you’re familiar with similar frameworks such as Redux, you shouldn’t find anything too surprising here. If you haven’t dealt with any Flux-based state management frameworks before, please pay close attention.
The Vuex Store
The store provides a centralized repository for shared state in Vue apps. This is what it looks like in its most basic form:
// src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
// put variables and collections here
},
mutations: {
// put sychronous functions for changing state e.g. add, edit, delete
},
actions: {
// put asynchronous functions that can call one or more mutation functions
}
})
After defining your store, you need to inject it into your Vue.js application like this:
// src/main.js
import store from './store'
new Vue({
store,
render: h => h(App)
}).$mount('#app')
This will make the injected store instance available to every component in our application as this.$store
.
Working with State
Also referred to as the single state tree, this is simply an object that contains all front-end application data. Vuex, just like Redux, operates using a single store. Application data is organized in a tree-like structure. Its construction is quite simple. Here’s an example:
state: {
products: [],
count: 5,
loggedInUser: {
name: 'John',
role: 'Admin'
}
}
Here we have products
that we’ve initialized with an empty array, and count
, which is initialized with the value 5. We also have loggedInUser
, which is a JavaScript object literal containing multiple fields. State properties can contain any valid datatype from Booleans, to arrays, to other objects.
There are multiple ways to display state in our views. We can reference the store directly in our templates using $store
:
<template>
<p>{{ $store.state.count }}</p>
</template>
Or we can return some store state from within a computed property:
<template>
<p>{{ count }}</p>
</template>
<script>
export default {
computed: {
count() {
return this.$store.state.count;
}
}
}
</script>
Since Vuex stores are reactive, whenever the value of $store.state.count
changes, the view will change as well. All this happens behind the scenes, making your code look simple and cleaner.
The mapState
Helper
Now, suppose you have multiple states you want to display in your views. Declaring a long list of computed properties can get verbose, so Vuex provides a mapState helper. This can be used to generate multiple computed properties easily. Here’s an example:
<template>
<div>
<p>Welcome, {{ loggedInUser.name }}.</p>
<p>Count is {{ count }}.</p>
</div>
</template>
<script>
import { mapState } from 'vuex';
export default {
computed: mapState({
count: state => state.count,
loggedInUser: state => state.loggedInUser
})
}
</script>
Here’s an even simpler alternative where we can pass an array of strings to the mapState
helper function:
export default {
computed: mapState([
'count', 'loggedInUser'
])
}
This version of the code and the one above it do exactly the same thing. You should note that mapState
returns an object. If you want to use it with other computed properties, you can use the spread operator. Here’s how:
computed: {
...mapState([
'count', 'loggedInUser'
]),
parity: function() {
return this.count % 2 === 0 ? 'even' : 'odd'
}
}
Getters
In a Vuex store, getters are the equivalent to Vue’s computed properties. They allow you to create derived state that can be shared between different components. Here’s a quick example:
getters: {
depletedProducts: state => {
return state.products.filter(product => product.stock <= 0)
}
}
Results of getter
handlers (when accessed as properties) are cached and can be called as many times as you wish. They’re also reactive to state changes. In other words, if the state it depends upon changes, the getter
function is automatically executed and the new result is cached. Any component that has accessed a getter
handler will get updated instantly. This is how you can access a getter
handler from a component:
computed: {
depletedProducts() {
return this.$store.getters.depletedProducts;
}
}
The mapGetters
Helper
You can simplify the getters
code by using the mapGetters
helper:
import { mapGetters } from 'vuex'
export default {
//..
computed: {
...mapGetters([
'depletedProducts',
'anotherGetter'
])
}
}
There’s an option for passing arguments to a getter
handler by returning a function. This is useful if you want to perform a query within the getter
:
getters: {
getProductById: state => id => {
return state.products.find(product => product.id === id);
}
}
store.getters.getProductById(5)
Do note that each time a getter
handler is accessed via a method, it will always run and the result won’t be cached.
Compare:
// property notation, result cached
store.getters.depletedProducts
// method notation, result not cached
store.getters.getProductById(5)
Changing State with Mutations
An important aspect of the Vuex architecture is that components never alter the state directly. Doing so can lead to odd bugs and inconsistencies in the app’s state.
Instead, the way to change state in a Vuex store is by committing a mutation. For those of you familiar with Redux, these are similar to reducers.
Here is an example of a mutation that increases a count
variable stored in state
:
export default new Vuex.Store({
state:{
count: 1
},
mutations: {
increment(state) {
state.count++
}
}
})
You can’t call a mutation handler directly. Instead, you trigger one by “committing a mutation” like this:
methods: {
updateCount() {
this.$store.commit('increment');
}
}
You can also pass parameters to a mutation:
// store.js
mutations: {
incrementBy(state, n) {
state.count += n;
}
}
// component
updateCount() {
this.$store.commit('incrementBy', 25);
}
In the above example, we’re passing the mutation an integer by which it should increase the count. You can also pass an object as a parameter. This way, you can include multiple fields easily without overloading your mutation handler:
// store.js
mutations: {
incrementBy(state, payload) {
state.count += payload.amount;
}
}
// component
updateCount() {
this.$store.commit('incrementBy', { amount: 25 });
}
You can also perform an object-style commit that looks like this:
store.commit({
type: 'incrementBy',
amount: 25
})
The mutation handler will remain the same.
The mapMutations
Helper
Similar to mapState
and mapGetters
, you can also use the mapMutations
helper to reduce the boilerplate for your mutation handlers:
import { mapMutations } from 'vuex'
export default{
methods: {
...mapMutations([
'increment', // maps to this.increment()
'incrementBy' // maps to this.incrementBy(amount)
])
}
}
On a final note, mutation handlers must be synchronous. You can attempt to write an asynchronous mutation function, but you’ll come to find out later down the road that it causes unnecessary complications. Let’s move on to actions.
Actions
Actions are functions that don’t change the state themselves. Instead, they commit mutations after performing some logic (which is often asynchronous). Here’s a simple example of an action:
//..
actions: {
increment(context) {
context.commit('increment');
}
}
Action handlers receive a context
object as their first argument, which gives us access to store properties and methods. For example:
context.commit
: commit a mutationcontext.state
: access statecontext.getters
: access getters
You can also use argument destructing to extract the store attributes you need for your code. For example:
actions: {
increment({ commit }) {
commit('increment');
}
}
As mentioned above, actions can be asynchronous. Here’s an example:
actions: {
incrementAsync: async({ commit }) => {
return await setTimeout(() => { commit('increment') }, 1000);
}
}
In this example, the mutation is committed after 1,000 milliseconds.
Like mutations, action handlers aren’t called directly, but rather via a dedicated dispatch
method on the store, like so:
store.dispatch('incrementAsync')
// dispatch with payload
store.dispatch('incrementBy', { amount: 25})
// dispatch with object
store.dispatch({
type: 'incrementBy',
amount: 25
})
You can dispatch an action in a component like this:
this.$store.dispatch('increment')
The mapActions
Helper
Alternatively, you can use the mapActions
helper to assign action handlers to local methods:
import { mapActions } from 'vuex'
export default {
//..
methods: {
...mapActions([
'incrementBy', // maps this.increment(amount) to this.$store.dispatch(increment)
'incrementAsync', // maps this.incrementAsync() to this.$store.dispatch(incrementAsync)
add: 'increment' // maps this.add() to this.$store.dispatch(increment)
])
}
}
Re-build Counter App Using Vuex
Now that we’ve had a look at the core concepts of Vuex, it’s time to implement what we’ve learned and rewrite our counter to make use of Vue’s official state management solution.
If you fancy a challenge, you might like to have a go at doing this yourself before reading on …
When we generated our project using Vue CLI
, we selected Vuex
as one of the features. A couple of things happened:
Vuex
was installed as a package dependency. Check yourpackage.json
to confirm this.- A
store.js
file was created and injected into your Vue.js application viamain.js
.
To convert our “local state” counter app to a Vuex application, open src/store.js
and update the code as follows:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
count: 0
},
getters: {
parity: state => state.count % 2 === 0 ? 'even' : 'odd'
},
mutations: {
increment(state) {
state.count++;
},
decrement(state) {
state.count--;
}
},
actions: {
increment: ({ commit }) => commit('increment'),
decrement: ({ commit }) => commit('decrement'),
incrementIfOdd: ({ commit, getters }) => getters.parity === 'odd' ? commit('increment') : false,
incrementAsync: ({ commit }) => {
setTimeout(() => { commit('increment') }, 1000);
}
}
});
Here we can see how a complete Vuex store is structured in practice. Please go back over the theory part of this article if anything here is unclear to you.
Next, update the src/components/Counter.vue
component by replacing the existing code within the <script>
block. We’ll switch the local state and functions to the newly created ones in the Vuex store:
import { mapState mapGetters, mapActions } from 'vuex'
export default {
name: 'Counter',
computed: {
...mapState([
'count'
]),
...mapGetters([
'parity'
])
},
methods: mapActions([
'increment',
'decrement',
'incrementIfOdd',
'incrementAsync'
])
}
The template code should remain the same, as we’re sticking to the previous variable and function names. See how much cleaner the code now is.
If you don’t want to use the state and getter map helpers, you can access the store data directly from your template like this:
<p>
Clicked {{ $store.state.count }} times! Count is {{ $store.getters.parity }}.
</p>
After you’ve saved your changes, make sure to test your application. From an end-user perspective, the counter application should function exactly the same as before. The only difference is that the counter is now operating from a Vuex store.
See the Pen Vue Counter Using Vuex by SitePoint (@SitePoint) on CodePen.
Conclusion
In this article, we’ve looked at what Vuex is, what problem it solves, how to install it, as well as its core concepts. We then applied these concepts to refactor our counter app to work with Vuex. Hopefully this introduction will serve you well in implementing Vuex in your own projects.
It should also be noted that using Vuex in such a simple app is total overkill. However, in another article—Build a Shopping List App with Vue, Vuex and Bootstrap Vue—I’ll build a more complicated app to demonstrate a more real-world scenario, as well as some of Vuex’s more advanced features.
Frequently Asked Questions about Vuex for Beginners
What is the main purpose of Vuex in a Vue.js application?
Vuex is a state management pattern and library for Vue.js applications. It serves as a centralized store for all the components in an application, with rules ensuring that the state can only be mutated in a predictable fashion. It integrates well with the official devtools extension, providing features like zero-config time-travel debugging and state snapshot export/import.
How does Vuex differ from other state management libraries?
Vuex is specifically designed for Vue.js, which means it takes advantage of Vue.js’s reactivity system out of the box. This makes it easier to track changes in the state of your application and react accordingly. Vuex also provides a comprehensive set of tools and conventions to handle more complex state management scenarios, such as asynchronous actions and modules.
Can I use Vuex with other frameworks or libraries?
Vuex is specifically designed to work with Vue.js. While it’s technically possible to use Vuex with other frameworks or libraries, it’s not recommended because you won’t be able to take full advantage of Vuex’s integration with Vue.js’s reactivity system.
How do I install and set up Vuex in my Vue.js project?
Vuex can be installed via npm or yarn. Once installed, you can import it into your Vue.js project and set up a new Vuex store. The store is where you’ll define your state, mutations, actions, and getters.
What are Vuex mutations and how do they work?
Mutations are functions that directly mutate the state of your Vuex store. They are the only way to change state in a Vuex store. Each mutation has a string type and a handler function, which is where we perform actual state modifications, and it will receive the state as the first argument.
How do Vuex actions differ from mutations?
Actions are similar to mutations, but there are a few key differences. Actions commit mutations, rather than directly changing the state. Actions can contain arbitrary asynchronous operations, whereas mutations must be synchronous. This makes actions a good place to put complex business logic and asynchronous operations.
What are Vuex getters and how do I use them?
Getters are functions that compute derived state based on the store’s state. They can be thought of as computed properties for stores. You can access them as properties on the store’s getter object.
How do I organize my Vuex store for a large application?
For larger applications, Vuex allows you to divide your store into modules. Each module can contain its own state, mutations, actions, and getters, making it easier to manage and isolate different parts of your application’s state.
Can I use Vuex with Vue.js’s server-side rendering (SSR)?
Yes, Vuex works out-of-the-box with Vue.js’s server-side rendering. This allows you to pre-fetch data on the server and generate the final HTML on the server, reducing the time to first render for your users.
How do I test my Vuex store?
Vuex provides a way to directly mutate and track state changes in your store, making it easy to test. You can also use Vue.js’s official unit testing library, vue-test-utils, to test components that use Vuex.
I write clean, readable and modular code. I love learning new technologies that bring efficiencies and increased productivity to my workflow.