Vuejs Vuex State Management for Beginners

Reading Time 5 min

As your VueJS App grows, the number of components keeps growing, giving rise to the need for state management to communicate between components. Vuex is the recommended tool for State Management built specifically for Vuejs apps. Before proceeding, lets refresh a few concepts…

What is State?
State is like a container/Object that stores some value/values.

var count = 0;         // This can be considered as the state
function increment() { // function that modifies the state
  return ++count;
}
increment() // 1
increment() // 2

What is State management?
The way in which a state is modified is referred to as state management. There are different patterns/ways of modifying state used by modern Javascript libraries. ReactJS uses Flux-Redux, AngularJS uses Observable Data Services and VueJS uses Vuex.

What is Vuex

Vuex store state management in vue js
According to the official documentation,

Vuex is a state management pattern + library for Vue.js applications. It serves as a centralized store for all the components in an application. It ensures that the state can only be mutated in a predictable fashion.


What is a Store?

A Store is like a large object with getter functions and methods to register/unregister listeners. It has no setters. The only way data gets inside a store is through a callback which is registered via listeners. The state of the application is stored(saved) inside a store. Each time the state inside the store changes, an event is emitted alerting the view layer. The view layer then updates itself by requesting the new data(state) using the getters. Stores are usually singletons (Flux, Vuex) however it is possible to use instances.


One-way data flow

Vuex One way data flow
Vuex uses one-way data flow. i.e. State -> View -> Action. The state is the single source of data which is provided to the View layer. The View layer displays the current state of the Application. The View layer is capable of triggering actions. Actions are basically user inputs, like a button click. Actions are responsible for changes/mutations of the state. They change and update the state. This updated state is again fed back to the View layer which then displays the new state.


Vuex is specifically tailored for VueJS apps. This is how Vuex fits in your app:
vuex architecture

Getting Started

If you haven’t already installed Vuex, you can install it using npm or yarn package manager.

// install using npm
npm install vuex --save
// install using yarn
yarn add vuex

// include, use Vuex in your app
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)

By now, you now already know what store, state means. To update the state in a Vuex store, you need to commit the changes through callback methods. These callback methods are defined in an object named mutations. The code snippet below should help you understand better –

As seen, each time you commit mutations, the Vuex state gets updated. Now how do we get this updated state in our components?

Accessing Vuex state in Components

The easiest way to get state is to return the state from within a computed property of your component. So, basically, each computed property acts like a simple getter function in this context. Whenever the state changes, the computed property re-evaluates and updates the DOM.

As seen, we defined a computed property named count() that returns the count property in the state. But if we had multiple properties, then to retrieve those properties we would have to define many computed properties(i.e. many getters) and this can get quite tedious/redundant. Is there a way out? The mapState helper to the rescue!


The mapState Helper

The mapState helper generates computed properties/getters, thus saving you a few keystrokes. There are several ways of doing it (ref), but we will use the spread operator for now

import { mapState } from 'vuex'

  // ...
  computed: {
      ...mapState({    // use spread operator to merge this into computed properties object
          mycount: count => state.count  // gets 'count' from root-store
      }),
      anotherProperty() {
          return this.$store.state.xyz;
      }
  }
  // ... 
  // You can now directly access `mycount` in your component's template
  // template: `<div>{{ mycount }}</div>`
}

mapState maps the state in the root-store to the state in the component. So now, the value of count would be available in the Counter component via the mycount computed property. Note: while using the mapState helper, you might encounter build errors since the Spread Operator is a part of ES2018. To fix that, follow the tutorial to support ES2018 features.


Getters in the root-Store

When you wish to use computed properties in more than one component, you could use getters in the root-store instead of using the computed properties. “Why so?”, you exclaim! To answer that, lets consider a scenario. Suppose that you want to filter a list of items in the state and access it in your component(s), you would set the computed property as follows:

computed: {
  doneTodosCount () {
    return this.$store.state.todos.filter(todo => todo.done).length
  }
}

But if more than one component needs to make use of this, we will have to either duplicate the function in each component, or then extract it into a shared helper and import it in multiple places. Both of these solutions are not ideal. Hence, the use of getters in the root-store is preferred.

As seen above, getters defined inside the store are global and are available inside any component as this.$store.getters.xyz. You could also pass parameters to these getters.

// Getters in the root-store
getters: {
  // ...
  getTodoById: (state) => (id) => {
    return state.todos.find(todo => todo.id === id)
  }
}

// computed property inside any component
this.$store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }

For further clarification, check out the official docs for getters.

Understanding Vuex Mutations

Mutations are used to mutate(change) state in the store. Each mutation is a function that receives 2 parameters – the state and the payload. The payload parameter is optional. The most important thing to remember is that in Vuex, mutations are used only for synchronous operations(Ref). For asynchronous operations like ajax calls, Vuex uses Actions (covered later in the article). Coming back to mutations, they can be defined either in the root store or in the components.

Mutations in the root Store


As established earlier, when you commit the mutation, the payload parameter is optional and could be an object with multiple fields. There is another way to commit mutations using Object style commits. In this, you could directly pass an object with type: mutationName. A quick example would be:

/* object style mutation */
store.commit({
  type: 'increment',       // mutation name
  amount: 10               // payload
})
/* Usual mutation */ 
store.commit('increment',   // mutation name
 {amount: 10}               // payload
)

Mutations in the Components

You can commit mutations in components with this.$store.commit('xxx'), or alternatively use the mapMutations helper which maps mutations in your root store to your component’s methods. The syntax of mapMutations is exactly the same as mapState. i.e. we use spread operator to add the desired mutations to our component’s “methods” object.

import { mapMutations } from 'vuex'

export default {
  // ...
  methods: {
    // without mapMutations helper
    add() {
      this.$store.commit('increment'); 
    }
    // with mapMutations helper
    ...mapMutations({
      add: 'increment' // maps `this.add()` to `this.$store.commit('increment')`
    }),
    ...mapMutations([
      'increment',  // maps `this.increment()` to `this.$store.commit('increment')`
      'incrementBy' // maps `this.incrementBy(amount)` to `this.$store.commit('incrementBy', amount)`
    ])
  }
}

Now, in your component, you could directly use this.add() to commit your mutation. You can also commit mutations with payloads as shown above(this.incrementBy).


Understanding Vuex Actions

Actions are just like Mutations. The only difference is that they are used solely for asynchronous operations (like getting data from an API). Unlike mutations, actions do not modify the state directly. But instead, they commit mutations.

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    },
    incrementBy (state, payload) {
      state.count += payload
    }
  },
  actions: {
    increment (context) {            // action without payload
      setTimeout(() => {             // some asynchronous operation
        context.commit('increment');
      }, 1000);  
    },
    incrementBy (context, payload) { // action with payload
      setTimeout(() => {             // some asynchronous operation
        context.commit('incrementBy', payload);
      }, 1000);  
    }
  }
})

As you can see, each action is a method that receives 2 parameters – the context and the payload. Context is an object that has the same methods/properties of the root-store instance. The payload is the data that you want to send. It is an optional parameter. If you use ES2015 argument destructuring, you could simplify your action method to:

actions: {
  increment ({ commit }, payload) {     // argument destructuring of `context` parameter
    setTimeout(() => {                  // some asynchronous operation
        commit('incrementBy', payload); // commit mutation
      }, 1000);     
  }
}

Calling/Triggering Actions

After defining these actions in your store, how do you trigger them? It’s done by using the store.dispatch('actionName') method. So, you could call actions in your component’s “methods” property

    // your component
    // ...
    methods {
        getAllCountries() {
            return this.$store.dispatch('getCountries')  // dispatch() is available in the root-store
        }
    }
    // ...

An alternate way is to use the mapActions helper. It is very similar to the mapMutations helper.

import { mapActions } from 'vuex'

export default {
  // ...
  methods: {
    ...mapActions({
      getAllCountries: 'getCountries' // maps `this.getAllCountries()` to `this.$store.dispatch('getCountries')`
    }),
    ...mapActions([
      'getCountries',     // maps `this.getCountries()` to `this.$store.dispatch('getCountries')`
      'getCountriesBy'    // maps `this.getCountriesBy(name)` to `this.$store.dispatch('getCountriesBy', name)`
    ])
  }
}

You could now directly call this.getAllCountries() to trigger the getCountries action.
Find below a fully functional example. We will use axios to make http requests which is also recommended by the VueJS Core team.

Now you realize how simple it is to manage state using Vuex Actions. You can also execute actions one after the other by returning a Promise in the actions. The dispatch() method supports this.

// ...
actions: {
  getCity({commit}, name) {
    return new Promise((resolve, reject) => {        // return Promise
       // make http request to get city, commit mutations
       resolve();
    });
  },
  getCountry({commit, dispatch}, name) {             // destructing object to get `commit`, `dispatch`
    return dispatch('getCity', name).then(() => {    // chain actions
        return new Promise((resolve, reject) => {    // return Promise
           // make http request to get country, commit mutations
           resolve();
        });
    });
  }
}
// ...

/*** Now in your component you can trigger actions ***/
 methods: {
   ...mapActions({
     getMyCountry: 'getCountry' // maps `this.getMyCountry()` to `this.$store.dispatch('getCountry')`
   })
 },
 created: {
   this.getMyCountry()          // call/trigger action
 }

Each time you call the getCountry() action, it will also call the getCity() action. This way you can chain multiple ajax calls (async operations).

Vuex Modules

We were using a single store until now. But as your app grows, your store could get quite large. To deal with this, Vuex allows you to divide the store into modules. Each module can have its own state, getter, mutation and actions. Each module can have a sub-module as well. A sub-module behaves exactly like a module.

const moduleA = {
  namespaced: true,
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  namespaced: true,
  state: { ... },
  mutations: { ... },
  actions: { ... }
}
// initialize the store and its modules
const store = new Vuex.Store({
  modules: {
    first: moduleA,
    second: moduleB
  }
})

store.state.first  // -> `moduleA`'s state
store.state.second // -> `moduleB`'s state

By default, actions, mutations and getters are registered in the global namespace. But by setting namespaced: true in each module, we have marked it as namespaced.

Getters, Mutations and Actions in Modules

Every getter, mutation and action in each module receives state as the first parameter. This state is the local state of that module. The root-state is passed as the 3rd parameter. Here’s how it looks like –

const moduleA = {
  state: { count: 0 },
  mutations: {
    incrementBy (state, amount) { // `state` -> moduleA's state
      state.count += amount       // `amount` -> payload
    }
  },
  getters: {            // For getters, `rootState` is passed as 3rd parameter
    doubleCount (state, otherGetters, rootState) {   // `state` -> moduleA's state
      return state.count * 2
    }
  },
  actions: {            // For actions, `rootState` is present in the `context` object.            
    increaseCount ({ state, commit, rootState }, amount) { // `state` -> moduleA's state
      commit('incrementBy', amount)
    }
  }
}

/*** To call a module's action from a component ***/
// ...
created() {
  this.$store.dispatch('moduleA/increaseCount', 5);   // calls moduleA's increaseCount() action
}
// ...

You can also register modules dynamically. Feel free to checkout the official docs on modules.

Application Structure

Vuex team recommends to use the following application structure for your Vue apps, however they don’t enforce this upon you.
vuex application structure

Last of all, do not forget to also see their excellent Shopping Cart Example. Hope this helps 🙂


References

2 comments

Leave a Reply