Vue.js explained through pokemon #3 Vuex: state management

Maxim Kerstens

This is the third article in my tutorial series that tries to explain Vue.js concepts by recreating the battle scene from Pokemon.

Original tutorial
#1 Single file components
#2 Attacks: $refs, Promises & event bus
#3 Vuex: state management
#4 Damage calculations
#5 Transitions & animation

In this article we’re going to start tackling our state in a different way.

Let’s continue on the event-based implementation of last tutorial:

In the previous articles we talked about isolated state, the data that each component carried with it. This is great for small apps, but when your app starts growing you’ll soon notice your data methods filling up, making it harder to see what data is contained.

Our pokemon app is still small, but once we start adding a more traditional attack system it’ll grow quickly, when we add more pokemon and items it’ll become unmanageable if we keep all our stored data together in a component.

Our main focus is separating the data while still keeping everything reactive and logically stored. Preferably by the means of a common standard, so friends or colleagues can jump in when needed.

Enter Vuex, it’s a state management 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.

This brings a bunch of benefits, one thing you should note is that you won’t be able to change Vuex state directly, unlike local state, to ensure the predictability.

Instead you can commit mutations to the state tree or dispatch actions.

Mutations are functions that directly change a specific part of the state tree.

Actions are meant to commit mutations, they have a more async-nature to them. An action could do a request to your server, take the required data from its response and commit what is needed. In the simplest of uses it can be used to commit multiple mutations.

But wait, there’s something missing, what about computed properties?
Vuex offers getters as an alternative.

Retrieving getters is a little more verbose than computed properties, but I personally don’t mind the tradeoff.

Vuex state changes

Note: As a general rule you should presume that everything you define in a store shouldn’t be accessed directly. There are wrappers in place.

By this I mean that within actions, getters or mutations you shouldn’t try to access the state, getters, mutations or actions through ‘this’.

Let’s rethink our app’s state and translate it to Vuex.

New data structure

Let’s begin in App.vue, at the moment we have our player and opponent pokemon grouped and hard coded.

We’ll want to change that to having a pokedex and selecting the 2 pokemon from that list. This will also affect the pokemon component.

We’ll also want to separate the fightOptions computed property into a getter.
We’ll add a mutation that allows us to change a player’s or opponent’s HP.
Lastly, resetting all the pokemon’s HP will have to made into an action.

Since we’re extracting our data to a global store, we don’t need to assign a pokemon to a pokemon component, we’ll just retrieve it in the component itself.

In Pokemon.vue We’ll want to change any statement that tries to change the parent component’s state. This means we’ll have to create a mutator for handling HP changes.

Store definition overview

That’s nice and all, but let’s convert that to actual code.

Let’s pull in vuex

Defining the store

While you’re waiting for that to install let’s create a ‘store’ folder, this is where we’ll define our Vuex store.
Within it we’ll also add a ‘data’ folder, we’ll make our pokedex.js there:

Feel free to add any pokemon you’d like, I used bulbapedia’s generation 1 chart for stats and attacks.

Let’s go back to the ‘store’ folder and get into defining our state.

We’ll create an index.js file, here we’ll import Vuex and connect it with Vue.

Next we’ll define our store with the needed state, getters, mutations and actions:

Remember when I said nothing should be accessed directly?

Each of our getters and mutation methods will always get state as their first parameter. This will always be the most recent version of the state we have defined.

The action that we defined will get the state’s context assigned, this means we actually get a reference to the store. We can access the state through this, as well as other actions, mutations and getters.

If we want to retrieve or change anything we’ll have to do it through that first parameter.

Most of the comments explain enough, however I’m using object destructuring for setHP and reset.

The reason for this is because mutations only get 2 parameters.
If you want to assign more data you’ll have to group it in an object.
object destructuring is a nice way to show what properties you’re expecting.

Glue it in

Now that we have our store defined, let’s make use of it.

It’s time to open main.js, we’ll import the store and assign it to our root Vue definition.

Once the store has been assigned, you can access it through this.$store from the instance it’s been assigned to, as well as every child instance (components).

Accessing the store

We can access anything we’ve defined in our store, state an getters are pretty straight forward, mutations can be called through commit:

  • state: this.$store.state.property
  • getters: this.$store.getters.property
  • mutations: this.$store.commit(‘mutation’, payload)
  • actions: this.$store.dispatch(‘action’, payload)

Remember, we can’t set values on state and getters, that’s what mutations and actions are for.

You can reference state and getters as shown above, however you could also map them as computed properties, that way you can reference them in your Vue instance like you’re used to doing.

The same is true for mutations and actions, you would reference these in your Vue instance’s methods.

You would end up with something like this:

That works, but Vuex comes with a few helpers that can reduce our code; mapState, mapGetters, mapMutations and mapActions.

These helpers use the same footprint for their parameters, i’ll show you a few examples with mapState and mapGetters:

I’m using the spread operator here because those functions return an object, both object need to be merged into the computed property object.

Implementing the store

Let’s start by opening App.vue, first let’s get rid of the pokemon attribute on the pokemon components

On to the script tag, let’s import the Vuex helper we need, just like in the previous example.

We’ll start by cleaning our data method and remove everything until battleText, battleOptions and menu are left.

We’ll map the needed computed properties (player and playerAttacks).

While we’re at it, let’s change all the strings that references this.player.pokemon.name to literal strings, so they’re cleaner.

We’ll also map the reset action as resetPokemon in our methods and replace the old code that reset the pokemon’s HP directly.

Lastly we’ll also need to find a way to actually set the player and opponent pokemon.

Vue.js has various lifecycle hooks, we’re already using one, the mounted method on the Vue instance definition, where we set up our event listeners.

We’re going to be using the create hook to setup the pokemon. By doing it in this hook we’ll be sure that the pokemon are set before App.vue gets mounted (displayed in the DOM). This also means we’re setting up the global store before it gets accessed through the computed properties we setup.

As you can see in the snippet above, we placed the created method right above our existing mounted method.

Now that our battle stage is ready, let’s dive into Pokemon.vue.

Let’s go over our to do list:

  • Let’s remove the pokemon prop, it’s not needed anymore
  • We’ll also setup computed properties for the local pokemon (the one this component was set up for) and it’s opponent.
  • The pokemon image :src needs to change since we switched to the pokemon.images{front,back} structure instead of just a string. We’ll setup a computed property for this.
  • All references to this.pokemon and this.opponent need to be replaced.
  • Changes made to pokemon HP need to be changed to using the setHP mutator.
  • We’ll also need to change the way the width of the HP bar is calculate since we’re there’s a slim chance the pokemon will have a 100 HP stat.

Under the data method you can see that I renamed the opponent property to otherPokemon, I did this because I’m using state mapping to retrieve the opponent data from the global state under the computed properties.

You can see we map 2 properties from the global state under computed properties:

  1. local maps to the pokemon that’s assigned to this component.
  2. opponent maps to the pokemon it’s fighting.

Don’t forget that we defined both these properties in our store to be an object that carries an hp property (the hp that we reduce with attacks) and a pokemon property that stores the pokemon definition.

The image property has also been added to decide on which image to use (an opponent would need the image that shows the pokemon from the front, while the player’s pokemon needs to be shown from the back).

Under methods we map the setHP mutator for easy access and implemented it in the attack method, where we used to directly change the opponent’s HP.

For preparation for the next article I also created a calculateDamage method, currently it just returns the attack’s power, unchanged. But we’ll want to add some calculations later based on stats.

Remember the getter’s we defined in our global store? opponentAttacks, playerAttacks.

In pickRandomAttack we’re using the getter we need to retrieve a list of attack names from the local pokemon.

That’s it, we’ve successfully yanked out most of our state and stored it globally.

As always you can find the code on github:

In the next article we’ll focus on how the traditional pokemon game calculates damage based on the pokemon’s stats & type.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade