Create your account

Already have an account? Login here

Note: By joining, you will receive periodic emails from Coursetro. You can unsubscribe from these emails.

Create account

A Vuex Tutorial by Example - Learn Vue State Management

By Gary simon - Mar 17, 2018

Chances are, after you've learned basic Vue fundamentals (watch our free Vue 2 course on that), you're going to need to learn Vuex.

Because I'm a big fan of teaching by example, in this tutorial, you're going to learn Vuex by creating an actual app that does something. This way, you have some context instead of just learning theory and all of that stuff!

You should be familiar with Vue before proceeding, as this is an intermediate topic as it pertains to Vue.

Let's get started!

If you prefer watching a video..

Be sure to Subscribe to the Official Coursetro Youtube Channel for more videos.

What is Vuex?

Vuex handles what's called state management. State management is where the government takes your money and provides you with bad services -- no, I kid. The state as it pertains to Vuex is simply your application's data.

This includes stuff like your variables/properties, arrays, objects, etc.. A very simple Vue application wouldn't likely need to use Vuex because you can define all of that stuff in the component. It becomes problematic though when your Vue app is larger with a lot of components and a lot of properties. Vuex solves this problem by designating a central location from which the state data is stored, modified and accessed.

Alright, let's get our hands dirty.

Starting the Project

We're going to use the Vue CLI to create our project. At the time of writing this, it's at version 3.0.0-beta.6. As long as your Vue CLI is at or beyond version 3, you will be able to issue the vue create command to start a project.

In your projects folder, run the following command:

> vue create vuestate

Upon running this, you will be presented with the following prompt:

Vue CLI v3.0.0-beta.6
? Please pick a preset:
  default (babel, eslint)
> Manually select features

We want to manually select features, at which point you want to use your keyboard up and down keys and the spacebar to select the router and vuex as shown below:

? Check the features needed for your project:
 ( ) TypeScript
 ( ) Progressive Web App (PWA) Support
 (*) Router
>(*) Vuex
 ( ) CSS Pre-processors
 ( ) Linter / Formatter
 ( ) Unit Testing
 ( ) E2E Testing

Hit enter. It will ask you where you want to store config, and just leave it at In dedicated config files. Hit enter, select n if you don't want to save this preset, and it will begin installing the new vue app.

Hop into the folder and serve the app:

> cd vuestate && yarn serve

Great! Visit http://localhost:8080 in your browser and you're all set.

The Vue Devtools Chrome Plugin

Although not a requirement for this tutorial, the Vue DevTools extension for Chrome helps you debug your Vue.js app, and allows us to see what's happening with the state when Vuex is used.

Install the extension and hit CTRL-SHIFT-I (i) and at the very end of the list of tabs, you will see "Vue". Click on it and select the clock icon (Vuex) and you should see something similar as shown below:

There we go!

Defining a State Property

Open up our new Vue project in your code editor and visit the /src/store.js file. This is a file that's generated by the Vue CLI when we selected the Vuex option. This here is essentially Vuex. It's where we will define our data, mutations, actions, getters and all of that fancy stuff.

For now, let's take a look at the State section. Like I mentioned earlier, the state is where your data is defined. So, let's define a property inside of state which will allow us to set a title for our app:

  state: {
    title: 'My Custom Title'
  },

Simple enough. We can add other stuff like arrays and objects, which we'll do later.

Save this file and visit the /src/components/HelloWorld.vue. Adjust the template section as follows:

<template>
  <div class="hello">
    <div class="left">
      <h1>{{ title }}</h1>
    </div>
    <div class="right">

    </div>
  </div>
</template>

Next, in the script section, adjust it to match the following:

<script>
import { mapState } from 'vuex'

export default {
  name: 'HelloWorld',
  computed: mapState([
    'title'
  ])
}
</script>

First we're importing mapState, which is a helper that allows us to access the state from Vuex. This will give us access to our title property we created earlier.

Then, we're adding computed and calling mapState and passing in an array with the name of the state property we defined.

This retrieves the value of title and also allows us to reference it by the same name title through interpolation in the template section. If you save the file, you will see it now displays the title:

Notice in the Vue Devtools panel it shows our single state property. Awesome.

You can also pass in an object instead of an array with mapState, which allows you to do a few other things:

// Template Adjustment:
  <h1>{{ custom }}</h1>

// Logic:
  computed: mapState({
    custom: 'title'
  })

Here we've defined a different alias called custom.

Chances are, you're going to have more than just mapState() as a computed property. In this case, you need to use what's called a Object Spread Operator. You do this adding three periods before mapState and using it as you normally would.

Make the following adjustment:

export default {
  name: 'HelloWorld',
  computed: {
    ...mapState([
      'title'
    ]),
    // Other properties
  }
}

Great! Let's go back and define another state property as an array in /src/store.js:

  state: {
    title: 'My Custom Title',
    links: [
      'http://google.com',
      'http://coursetro.com',
      'http://youtube.com'
    ]
  },

And just in the same way, let's access it through mapState and render this list of links in the template:

    <div class="left">
      <h1>{{ title }}</h1>

      <ul>
        <li v-for="(link, index) in links" v-bind:key="index">
          {{ link }}
        </li>
      </ul>
    </div>

And in the script section:

    ...mapState([
      'title',
      'links'
    ]),

Wala!

Vuex Getters

To this point, we've been using the mapState helper to directly access the data contained in the state. This is fine when no computations need to be made on the data itself, but when you're dealing with multiple components, you may find yourself duplicating code unnecessarily when transformations need to be made on the data.

This is where Getters come in handy. Let's say for instance we want to count the number of links in our array. We can use a custom Getter for this.

Visit the /src/store.js and add the following:

  state: {
    // Code removed for brevity
  },
  getters: {
    countLinks: state => {
      return state.links.length
    }
  },
  mutations: {},
  actions: {}

Here, we're creating a getter called countLinks. Then, we're passing in access to the state, and returning the .length (counting the number of links in the array) on the links array contained in the state. You could do any number of things here, which could obviously be much more complex.

Going back to the original /src/components/HelloWorld.vue file, we could easily access this new getter from within this component, but I want to create another component from which we can access this getter simply for demonstration purposes. The whole point of Vuex is to allow multiple components to access the same state management, so I felt it would be necessary to create another component for this vuex tutorial.

With that said, let's make the following adjustments to our current .vue file:

<template>
  <div class="hello">
    <div class="left">
      <!-- Other code removed for brevity -->
    </div>
    <div class="right">
      <stats />  <!-- Add this -->
    </div>
  </div>
</template>

<script>
import Stats from '@/components/Stats.vue'  // Add this
import { mapState } from 'vuex'

export default {
  name: 'HelloWorld',
  components: {                             // Add this
    Stats
  },
  computed: {
    // Code removed for brevity
  }
}
</script>

Next, copy the current HelloWorld.vue file and rename it to Stats.vue. Inside of it, make the following adjustments:

<template>
  <div class="stats">
    <h1>A different component</h1>
    <p>There are currently {{ countLinks }} links</p>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'

export default {
  name: 'Stats',
  computed: {
    ...mapGetters([
      'countLinks'
    ]),
  }
}
</script>

This time, instead of importing mapState, we're importing mapGetters which will give us access to our custom getters.

Then we're using it just the same as mapState! We call mapGetters() and pass in the name of the getter we want to access and displaying it via interpolation in the template:

Let's take a break and make this look better

Okay, we have to take a break for a second -- this thing is absolutely ugly. The designer in myself needs to make this app look better.

Do me a favor and copy and paste the following CSS rulesets in the appropriate files..

/src/components/HelloWorld.vue:

<style>
  html, #app, .home {
    height: 100%;
  }
  body {
    background-color: #F4F4F4;
    margin: 0;
    height: 100%;
  }

  .hello {
    display: grid;
    grid-template-columns: repeat(2, 50%);
    grid-template-rows: 100%;
    grid-template-areas:
      "left right";
    height: 100%;
  }

  .left, .right {
    padding: 30px;
  }

  ul {
    list-style-type: none;
    padding: 0;
  }
  ul li {
    padding: 20px;
    background: white;
    margin-bottom: 8px;
  }

  .right {
    grid-area: right;
    background-color: #E9E9E9;
  }

</style>

And in /src/App.vue:

<template>
  <div id="app">
    <router-view/>
  </div>
</template>

<style>
  #app {
    font-family: 'Avenir', Helvetica, Arial, sans-serif;
  }
</style>

Also, visit /src/views/Home.vue and adjust the template:

<template>
  <div class="home">
    <HelloWorld />
  </div>
</template>

Save it:

Ah, there we go, much better. Let's get back to it.

Vuex Mutations

To this point, we've only added and displayed data from the state. What about when we want to change the state data? That's where mutations come into play.

In our project, let's say we want to add a new link with a form input field.

We have to first create a mutation in the /src/store.js file:

  mutations: {
    ADD_LINK: (state, link) => {
      state.links.push(link)
    }
  },

We're naming this mutation ADD_LINK and passing in the state, and a payload named link. link will be the user-submitted value.

Then, we're simply pushing the link value to the links array defined in the state.

In our /src/HelloWorld.vue file, make the following adjustments to the template:

<h1>...</h1>

<!-- Add the form here -->

<form @submit.prevent="addLink">
   <input class="link-input" type="text" placeholder="Add a Link" v-model="newLink" />
</form>

<ul>...</ul>

Nothing fancy is happening here. We're just creating a form that will call a method addLink and binding the input class to a property called newLink.

Let's focus on the script section now:

<script>
import Stats from '@/components/Stats.vue'
import { mapState, mapMutations } from 'vuex'  // Add mapMutations

export default {
  name: 'HelloWorld',
  data() {                                     // Add this:
    return {
      newLink: ''
    }
  },
  components: {}, // Removed for brevity
  computed: {}, // Removed for brevity
  methods: {                                   // Add this:
    ...mapMutations([
      'ADD_LINK'
    ]),
    addLink: function() {
      this.ADD_LINK(this.newLink)
      this.newLink = ''
    }
  }
}
</script>

Okay, the part we want to focus on is inside methods: { }. We're using the mapMutations helper to import our ADD_LINK mutation, and then in the addLink() method, we call it and pass in this.newLink.

This will work now, but let's add a ruleset to make the input look better:

  input {
    border: none;
    padding: 20px;
    width: calc(100% - 40px);
    box-shadow: 0 5px 5px lightgrey;
    margin-bottom: 50px;
    outline: none;
  }

Great! Give it a try!

Let's add a couple more mutations, except this time, we'll use Actions to perform the mutations.

Vuex Actions

Calling mutations directly in the component is for synchronous events. Should you need asyncronous functionality, you use Actions.

This time, we'll use an Action to call upon a mutation that will remove a link.

In /src/store.js add the following mutation and action:

  mutations: {
    ADD_LINK: (state, link) => {
      state.links.push(link)
    },
    REMOVE_LINK: (state, link) => {        // Add this:
      state.links.splice(link, 1)
    }
  },
  actions: {
    removeLink: (context, link) => {       // Add this:
      context.commit("REMOVE_LINK", link)
    }
  }

So, nothing special is happening with REMOVE_LINK, but in actions, we're creating an action named removeLink in which the context (context simply provides you with the same methods and properties on the store instance), and the payload (link index) is passed.

Then we call context.commit where we call upon the REMOVE_LINK mutation and pass to it the link index. This seems redundant, but it's necessary for asynchronous operations.

Next, visit HelloWorld.vue and add the following to the template:

<ul>
  <li v-for="(link, index) in links" v-bind:key="index">
    {{ link }}
    <button v-on:click="removeLinks(index)" class="rm">Remove</button>  <!-- Add this -->
  </li>
</ul>

Then, we're going to include mapActions:

import { mapState, mapMutations, mapActions } from 'vuex'

And adjust our methods section:

  methods: {
    ...mapMutations([
      'ADD_LINK'
    ]),
    ...mapActions([                  // Add this
      'removeLink'
    ]),
    addLink: function() {
      this.ADD_LINK(this.newLink)
      this.newLink = '';
    },
    removeLinks: function(link) {    // Add this
      this.removeLink(link)
    }
  }
}

As you can see, it's set up to work in much the same way as calling mutations.

Let's add a quick ruleset for that button:

  .rm {
    float: right;
    text-transform: uppercase;
    font-size: .8em;
    background: #f9d0e3;
    border: none;
    padding: 5px;
    color: #ff0076;
    cursor:pointer;
  }

Give it a go in the project:

Removing will now work.

This isn't that exciting though, let's try actually demonstrating the value of using Actions to perform asynchronous operations.

Going back to /src/store.js, let's create a new mutation and an action for removing all links with a click of a button:

  mutations: {
    // Others removed for brevity,
    REMOVE_ALL: (state) => {                     // Add this
      state.links = []
    }
  },
  actions: {
    removeLink: (context, link) => {
      context.commit("REMOVE_LINK", link)
    },
    removeAll ({commit}) {                       // Add this
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          commit('REMOVE_ALL')
          resolve()
        }, 1500)
      })
    }
  }

We have a new REMOVE_ALL mutation that empties out the array.

Then, we have a new removeAll action, and this time we're using argument destructuring to pass in commit, which makes code more simple.

We're creating a promise and simulating an operation that could take time, and calling REMOVE_ALL after 1.5 seconds.

We'll perform this operation in our /src/components/Stats.vue file:

<template>
  <div class="stats">
    <h1>A different component</h1>
    <p>There are currently {{ countLinks }} links</p>

    <button v-on:click="removeAllLinks">Remove all links</button>
    <p>{{ msg }}</p>
  </div>
</template>

In the script section:

<script>
import { mapGetters, mapMutations, mapActions } from 'vuex'

export default {
  name: 'Stats',
  data() {
    return {
      msg: ''
    }
  },
  computed: {
    ...mapGetters([
      'countLinks'
    ]),
  },
  methods: {
    ...mapMutations(['REMOVE_ALL']),
    ...mapActions(['removeAll']),
    removeAllLinks() {
      this.removeAll().then(() => {
        this.msg = 'They have been removed'
      });
    }
  }
}
</script>

The important part here is this.removeAll().then() - This will only be displayed once the action has been performed.

Let's also create a quick ruleset in this component's style section for the button:

button {
    padding: 10px;
    margin-top: 30px;
    width: 100%;
    background: none;
    border: 1px solid lightgray;
    outline: 0;
    cursor: pointer;
}

Try it out in the browser. Click Remove all links and in 1.5 seconds, the links will be removed and our message displayed!

Awesome! You can also integrate async await into your actions and call upon other actions. We'll save that for another tutorial, though.

Conclusion

Hopefully, you were able to learn a lot about Vue application management with Vuex in this tutorial. See you soon!


Share this post




Say something about this awesome post!