March 4, 2020

Introduction to State Management with Vuex

by John Au-Yeung

Introduction to State Management with Vuex

Any Vue app that is more than a few components big is going to have shared state.

Without any state management system, we can only share state between parent and child components.

This is useful, but it's limited. We need to be able to share state regardless of the relationship of the components.

To do this in a Vue app, we can use Vuex. It’s the official state management library for Vue apps.

In this article, we'll look at how to share state with Vuex.

Getting Started

To get Vuex into our Vue app, we have to include the Vuex library. Then, we create a Vuex store with the initial state, mutations, and getters.

For example, we can create a simple store for our Vuex to store the count state and use it in our app as follows:

index.js:

const store = new Vuex.Store({
  state: {
    result: {}
  },
  mutations: {
    fetch(state, payload) {
      state.result = payload;
    }
  },
  getters: {
    result: state => state.result
  }
});

new Vue({
  el: "#app",
  store,
  methods: {
    async fetch() {
      const res = await fetch("https://api.agify.io/?name=michael");
      const result = await res.json();
      this.$store.commit("fetch", result);
    }
  },
  computed: {
    ...Vuex.mapGetters(["result"])
  }
});

index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>App</title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="https://unpkg.com/vuex"></script>
  </head>
  <body>
    <div id="app">
      <button @click="fetch">Fetch</button>
      <p>{{result.name}}</p>
    </div>
    <script src="./index.js"></script>
  </body>
</html>

In the code above, we added the script tags for Vue and Vuex in index.html.

Then, we create a div with ID app to house our Vue app. Inside the div, we add the button to call a method to increment the count state.

Then, we show the count in the p tag.

In index.js, we write the code for the Vuex store by writing:

const store = new Vuex.Store({
  state: {
    result: {}
  },
  mutations: {
    fetch(state, payload) {
      state.result = payload;
    }
  },
  getters: {
    result: state => state.result
  }
});

The code above gives us a simple store that stores the result state initially set to an empty object.

Then, we added our fetch mutation in the mutations section, which takes the state object that has the Vuex state. We then get the data from the payload and parameter and set the state.result to payload each time the fetch action is dispatched.

Also, we added a getter for the result, which is a function that takes the state of the store and then returns the count from it.

After we create the store, we use it in our Vue app by adding the store property into the object we passed into the Vue constructor.

Additionally, we get the result state in our Vue app by using the mapGetters method, which maps our getter to the result computed property in our Vue app.

Then, we wrote a fetch method, which references the $store instance and calls commit with the mutation name passed in.

We passed in the ’fetch’ string to commit the increment mutation.

Finally, our button has the @click prop and it's set to the fetch method.

In the end, when we click on the Fetch button, we should see ‘michael’ displayed from the store’s data as the API data is fetched.

We can also pass in the action type and payload as an object into the commit method as follows:

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment(state, payload) {
      state.count += payload.amount;
    }
  },
  getters: {
    count: state => state.count
  }
});

new Vue({
  el: "#app",
  store,
  methods: {
    increment() {
      this.$store.commit({
        type: "increment",
        amount: 2
      });
    }
  },
  computed: {
    ...Vuex.mapGetters(["count"])
  }
});

In the code above, we changed mutations to:

mutations: {
    increment(state, payload) {
        state.count += payload.amount;
    }
},

Then the increment method is changed to:

increment() {
    this.$store.commit({
        type: "increment",
        amount: 2
    });
}

Mutations can only run synchronous code. If we want to run something asynchronously, we have to use actions.

Vuex can't keep track of mutations that are asynchronous since asynchronous code doesn’t run sequentially.

We can also use the mapMutations method like the mapGetters method for getters to map mutations.

To use the mapMutations method, we can write the following code:

index.js:

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment(state, payload) {
      state.count += payload;
    }
  },
  getters: {
    count: state => state.count
  }
});

new Vue({
  el: "#app",
  store,
  methods: {
    ...Vuex.mapMutations({
      increment: "increment"
    })
  },
  computed: {
    ...Vuex.mapGetters(["count"])
  }
});

In the code above, we mapped the increment mutation to the increment method in our Vue app with the following code:

...Vuex.mapMutations({
    increment: "increment"
})

It takes an argument for the payload as we can see in the @click listener of the Increment button.

Actions

Actions commit one or more mutations in any way we like.

We can add a simple action to our store and use it as follows:

index.js:

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment(state, payload) {
      state.count += payload;
    }
  },
  getters: {
    count: state => state.count
  },
  actions: {
    increment({ commit }, payload) {
      commit("increment", payload);
    }
  }
});

new Vue({
  el: "#app",
  store,
  methods: {
    ...Vuex.mapActions(["increment"])
  },
  computed: {
    ...Vuex.mapGetters(["count"])
  }
});

Protect your Vue App with Jscrambler

In the code above, we defined the increment action in the Vuex store as follows:

actions: {
    increment({ commit }, payload) {
        commit("increment", payload);
    }
}

Then we called mapActions in our Vue app as follows:

methods: {
    ...Vuex.mapActions(["increment"])
},

The increment method, which is now mapped to the increment action, still takes the payload that we pass in, so we can pass the payload to the commit function call in our increment action, which is then passed into the increment mutation.

We can also write an asynchronous action by returning a promise as follows:

index.js:

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment(state, payload) {
      state.count += payload;
    }
  },
  getters: {
    count: state => state.count
  },
  actions: {
    increment({ commit }, payload) {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          commit("increment", payload);
          resolve();
        }, 1000);
      });
    }
  }
});

new Vue({
  el: "#app",
  store,
  methods: {
    ...Vuex.mapActions(["increment"])
  },
  computed: {
    ...Vuex.mapGetters(["count"])
  }
});

In the code above, we have:

actions: {
    increment({ commit }, payload) {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          commit("increment", payload);
          resolve();
        }, 1000);
      });
    }
}

which returns a promise that commits the increment mutation after a second.

Composing Actions

We can compose multiple actions in one action. For example, we can create an action that dispatches multiple actions as follows:

index.js

const store = new Vuex.Store({
  state: {
    dog: {},
    breeds: { message: {} }
  },
  mutations: {
    setDog(state, payload) {
      state.dog = payload;
    },
    setBreeds(state, payload) {
      state.breeds = payload;
    }
  },
  getters: {
    dog: state => state.dog,
    breeds: state => state.breeds
  },
  actions: {
    async getBreeds({ commit }) {
      const response = await fetch("https://dog.ceo/api/breeds/list/all");
      const breeds = await response.json();
      commit("setBreeds", breeds);
    },
    async getDog({ commit }) {
      const response = await fetch("https://dog.ceo/api/breeds/image/random");
      const dog = await response.json();
      commit("setDog", dog);
    },
    async getBreedsAndDog({ dispatch }) {
      await dispatch("getBreeds");
      await dispatch("getDog");
    }
  }
});

new Vue({
  el: "#app",
  store,
  methods: {
    ...Vuex.mapActions(["getBreedsAndDog"])
  },
  computed: {
    ...Vuex.mapGetters(["breeds", "dog"])
  }
});

index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>App</title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="https://unpkg.com/vuex"></script>
  </head>
  <body>
    <div id="app">
      <button @click="getBreedsAndDog">Get Breeds and Dog</button>
      <p>{{Object.keys(breeds.message).slice(0, 3).join(',')}}</p>
      <img :src="dog.message" />
    </div>
    <script src="./index.js"></script>
  </body>
</html>

In the code above, we have the getBreeds and getDog, which are actions with one mutation commit each to the store.

Then we have getBreedsAndDog which is an action that dispatches the above two actions.

In index.html, we display all the states that are stored in the store.

We should retrieve the first 3 breed names and also the dog image that we got from the getBreed and getDog actions that were called by the getBreedsAndDog action.

The getBreedsAndDog action is mapped to the getBreedsAndDog method, so we can just call it to dispatch the action.

Modules

We can divide our store into modules to segregate actions, mutations, and getters.

For example, we can write two modules and use them as follows:

index.js:

const dogModule = {
  namespaced: true,
  state: {
    dog: {}
  },
  mutations: {
    setDog(state, payload) {
      state.dog = payload;
    }
  },
  getters: {
    dog: state => state.dog
  },
  actions: {
    async getDog({ commit }) {
      const response = await fetch("https://dog.ceo/api/breeds/image/random");
      const dog = await response.json();
      commit("setDog", dog);
    }
  }
};

const countModule = {
  namespaced: true,
  state: {
    count: 0
  },
  mutations: {
    increment(state) {
      state.count++;
    }
  },
  getters: {
    count: state => state.count
  }
};

const store = new Vuex.Store({
  modules: {
    dogModule,
    countModule
  }
});

new Vue({
  el: "#app",
  store,
  methods: {
    ...Vuex.mapActions("dogModule", ["getDog"]),
    ...Vuex.mapMutations("countModule", ["increment"])
  },
  computed: {
    ...Vuex.mapState({
      count: state => state.countModule.count,
      dog: state => state.dogModule.dog
    })
  }
});

index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>App</title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="https://unpkg.com/vuex"></script>
  </head>
  <body>
    <div id="app">
      <button @click="getDog">Get Dog</button>
      <button @click="increment">Increment</button>
      <p>{{count}}</p>
      <img :src="dog.message" />
    </div>
    <script src="./index.js"></script>
  </body>
</html>

In the code above, we defined the dogModule and countModule, which have the state, mutations, getters, and actions as we have before.

Then, to create our store, we write:

const store = new Vuex.Store({
  modules: {
    dogModule,
    countModule
  }
});

Then, when we map our actions and mutations, we have to specify the module as follows:

methods: {
    ...Vuex.mapActions("dogModule", ["getDog"]),
    ...Vuex.mapMutations("countModule", ["increment"])
},

Then, when we map getters, we have to write functions to get them from the right module as follows:

computed: {
    ...Vuex.mapState({
      count: state => state.countModule.count,
      dog: state => state.dogModule.dog
    })
}

Conclusion

We can store shared state in Vue apps with Vuex.

It has state to store the states, getters to get data from states, mutations to change data, and actions to run one or more mutations or other actions and also run them asynchronously.

We can map states into computed properties with mapGetters and also map mutations and actions with mapMutations and mapActions methods respectively.

Finally, we can divide our store into modules to segregate them into smaller pieces.


Before deploying your commercial or enterprise Vue apps to production, make sure you are protecting their code against reverse-engineering, abuse, and tampering by following our tutorial.