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.
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.
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()
.
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 hereapp.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.
const mutations = {}const actions = {}const getters = {}const state = () => { return {}}
// Vuex v3 storeconst store = new Vuex.Store({ state, mutations, actions, getters})
// Vuex v4 storeconst 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:
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.
<!-- 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.
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.
<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.
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.
<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.
<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:
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?