Learn Vuex Through A Tutorial

Vue.js

26/12/2020


In my previous blog post "Introduction To Vuex And State Management", I explained how state management with Vuex works. If you haven't read it, I highly recommend you do since this will build on top of it. It's a quick read and will make this tutorial easier to digest, in which we'll learn Vuex through a walk-through tutorial. 👨‍💻

Vuex 3 or Vuex 4

Are you using version 3 or 4? You're in luck! No matter the version, they are pretty much identical. This means any code we go through in this article works for both of them.

Meme about learning Vuex through a tutorial using different versions.

The devil is in the detail! 😈

Any differences will be clearly be highlighted, so don't worry about that. 😋

Setting up Vuex

Talking about differences, here's already one! 😂 It concerns the setup of the library. However, there is good news. This is where the differences end... at least for anything discussed in this blog post. For a full list, I recommend you consult this Vuex web page.

Ahem... so back to the topic. How exactly do they differ in their setup?

Vuex 3

In version 3, we install Vuex plugin with the use() function in line 5. Then, we create a store instance by calling the constructor Vuex.Store(). Next, this store gets passed into Vue, which allows us to then access it within our Vue application.

JAVASCRIPT
import Vue from 'vue'
import Vuex from 'vuex'
import App from './App.vue'
Vue.use(Vuex)
const store = new Vuex.Store({
state() {},
mutations: {},
actions: {},
getters: {}
})
new Vue({
render: h => h(App),
store, // <-- Pass it in here
}).$mount('#app')

Vuex 4

In version 4, the first difference is that we use a function called createStore() to instantiate the Vuex store. However, we still pass the same store configuration as an argument as we did in version 3. Likewise, we install the store with use().

JAVASCRIPT
import { createApp } from 'vue'
import { createStore } from 'vuex'
import App from './App.vue'
const store = createStore({
state() {},
mutations: {},
actions: {},
getters: {}
})
const app = createApp(App)
app.use(store) // <-- Use it here
app.mount('#app')

Keeping things consistent

In order to keep things consistent throughout the tutorial and avoid confusion as to whether I'm using version 3 or 4 syntax, I'll be using the following layout below. That is, I will be declaring any state, mutations, etc. outside of the store object. In general, you should do this as it lends to more readable and cleaner code. 🤓

In case you didn't know it yet, the store is basically a JavaScript object that contains all your getters, mutations, actions and state.

JAVASCRIPT
const mutations = {}
const actions = {}
const getters = {}
const state = () => {
return {}
}
// Vuex v3 store
const store = new Vuex.Store({
state,
mutations,
actions,
getters
})
// Vuex v4 store
const store = createStore({
state,
mutations,
actions,
getters
})

Declaring and fetching the state

This entire tutorial will revolve around one simple task: incrementing and decrementing a number. Pretty simple, right? Of course we won't use local storage, otherwise this tutorial would be over on a whim. 😅

We'll first start of with declaring our counter variable in the state as follows:

JAVASCRIPT
const state = () => {
return {
counter: 0
}
}

The next thing on our to-do list is to fetch the value using a computed property and display it in our App.vue component.

HTML
<!-- App.vue -->
<template>
<div>
<button>Increment</button>
<button>Decrement</button>
<p>{{ counter }}</p>
</div>
</template>
<script>
export default {
computed: {
counter() {
return this.$store.state.counter;
}
}
}
</script>

On line 15, you may have noticed that we're using $store. It is essentially a Vue property that is available anywhere throughout the app, which we can use to access any properties and methods in our store. Notice we also added an interpolation to display counter on line 7.

An alternative to directly reading counter from our central state would be using a getter function. So how would we do that and why? 🧐

Using Getters

To retrieve our value using a getter function, we'd first have to create one in our store.

JAVASCRIPT
const getters = {
getCounter(state) {
return state.counter
}
}

As you can see on line 2, getters automatically have access to the state from which we can return our counter value. Back in our App.vue file, we call the getter function getCounter in the computed 🖥 property named counter, replacing the line of code we wrote earlier.

HTML
<script>
export default {
computed: {
counter() {
return this.$store.getters.getCounter
}
}
}
</script>

So again, what's the advantage of using a getter? Well, DRY code my friend! 😊 A getter is for all intents and purposes a centralized computed property that can be shared among all your Vue components. If you ever need to make changes to a getter, you only need to do it once! However, there's nothing really stopping you from just using computed properties in your App that all duplicate the same logic. 😕

Manipulating the state

Our next objective is to make the buttons work, so whenever we click on one, our number is either incremented or decremented. As you'll (hopefully) know from reading my previous Vuex article, any changes we make to our state should be done through a mutation, which itself is called by an action.

JAVASCRIPT
const mutations = {
increment(state) {
state.counter++
},
decrement(state) {
state.counter--
}
}
const actions = {
increment(context) {
context.commit('increment')
},
decrement(context) {
context.commit('decrement')
}
}

Starting with mutations, we have two functions for incrementing and decrementing counter. Mutations automatically have access to the state which allow us to directly change the state.

Next, we have to create two action functions increment ➕ and decrement ➖, which call, aka commit, the mutation functions of the same name. Contrary to mutations, actions automatically have access to a context object, which let's us use this commit method.

The only thing left to do now is to call, aka dispatch, the action functions from our App.vue file. Similarly, $store gives us access to the dispatch functions to do so.

HTML
<script>
export default {
methods: {
increment() {
this.$store.dispatch('increment')
},
decrement() {
this.$store.dispatch('decrement')
}
},
computed: {...}
}
</script>

Don't forget to add @click="increment()" and @click="decrement()" to each respective button, otherwise nothing will happen. Et voilà, we've made it! 🤓

But the action doesn't end here yet, buddy...

Simplifying the code

Let's imagine you not only have to call two actions, but perhaps a few dozen, thousands or millions 🤯 (okay, maybe I'm exaggerating, but you get the point)! It's not hard to imagine that you will quickly lose oversight of what you're doing. But worry not, for there is a solution called mapActions. Similarly, equivalent maps exist for the other three.

HTML
<template>
<div>
<!-- Make sure to change the function names -->
<button @click="incrementAction()">Increment</button>
<button @click="decrementAction()">Decrement</button>
<p>{{ counter }}</p>
</div>
</template>
<script>
import { mapActions, mapState } from 'vuex'
export default {
computed: {
...mapState([
'counter'
])
},
methods: {
...mapActions({
incrementAction: 'increment',
decrementAction: 'decrement'
})
}
}
</script>

First of all, we have to import the relevant helpers into our Vue component, which are mapActions and mapState. Upon calling these helpers, we get access to Objects containing our state and actions. Notice ‼️ that they both make use of an ES6 syntax called the object spread operator (the ... if you weren't sure what I'm referring to).

You also have two options regarding how to reference your methods and properties. You can do so directly, as seen in mapState (pay attention to the square brackets), or indirectly through a custom name, as seen in mapActions (pay attention to the curly braces).

Adding function inputs

What if we want to pass in some arguments that our mutations or actions can mess around with? 🤾‍♂️ How would we do that in Vuex? It's quite simple, really. Going back to our store file, we do the following changes:

JAVASCRIPT
const mutations = {
increment(state, payload) {
state.counter += payload;
},
decrement(state, payload) {
state.counter -= payload;
}
},
const actions = {
increment(context, payload) {
context.commit('increment', payload);
},
decrement(context, payload) {
context.commit('decrement', payload);
}
}

We simply added another parameter called payload in each function. Naturally, we changed the code a bit in the mutations to make use of our new input. Next, we dispatch the actions giving an input with @click="incrementAction(2)" and @click="decrementAction(2)". In this scenario, whenever we click on a button, our counter will be either incremented or decremented by two.

In case you were wondering 🤔 how to pass in multiple inputs into our actions and mutations, you would simply pass in an object in the following manner: @click="incrementAction({ value: 2 })". Now, to get the value in your mutations, you would simply reference it with payload.value.

If you've made it this far, congratulations! 🎉 You're well on your way to mastering Vuex. However, it shouldn't come as a surprise that Vuex provides many other features I wasn't able to cover in this article, so why not go and check it out?


WRITTEN BY

Code and stuff