Vue.js 3 Vuex State Management Tutorial

In this Vue tutorial we learn how to use the Vuex state management API when provide and inject isn't enough.

We cover how to create and use a store, change and pass data to mutations, retreiving data with getters and async operations with actions.

Finally, we cover the mapper convenience methods and their alternative syntax.

Lesson Video

If you prefer to learn visually, you can watch this lesson in video format.


note Pinia is the new recommended state management system for Vue. We’ll leave this Vuex tutorial in place for completeness and because it’s very likely you’re still going to find a lot of Vuex code in enterprise-level applications.

Lesson Project

If you want to follow along with the examples in this lesson, you will need an app generated by the Vue CLI . If you already have one from a previous lesson, you can use it instead.

We can set up the Vuex package in multiple ways.

  • Add it to an existing project by manually installing it with npm.
  • Generate a new project and add the Vuex package. We cover this process at the end of this lesson .

What is Vuex?

As our application or team grows, managing global state with the provide and inject APIs can become more challenging.

Vuex is a state management solution that helps us overcome these challenges.

Application state and its management

An application’s state is how it synchronizes data among its components.

There are two main types of state.

  • Local state is the data we manage inside a component, that only affects that component.

    An example would be a menu container that’s activated with a button.

  • Global state is data that affects multiple components, or the entire application.

    An example would be a user’s authentication status, whether they are logged in or not.

While the provide and inject APIs give us an easy way to manage cross-component state, they do have some drawbacks.

  1. A component can quickly become bloated with data that’s not necessary for its template, but for other components.
  2. It’s not always immediately obvious where a certain component’s state is changed. This can lead to unpredictable behavior, specially within a team of developers.
  3. Unpredictable behavior is error-prone because a developer can accidentally update the wrong state, or miss an important update to a state.

This is where Vuex helps us manage our state.

  1. It detaches the state from a component and manages it in a different place, so components cannot become bloated with unnecessary data.
  2. It’s more predictable because there are clear rules to how state should be managed, updated and synced.
  3. That predictability makes it less error-prone because we have a clearly defined data flow through the application.

The Flux pattern

In 2014, Facebook open-sourced its Flux state management pattern to the community.

Since then, the community has created their own implementations of the pattern, most notably Redux , MobX and Vuex .

The Flux pattern is made up of 4 parts, organized as a one-way data pipeline.


Flux pattern

  1. An Action occurs in the View .
  2. That action is Dispatched to the Store .
  3. The store receives the action and determines any state changes that should occur.
  4. After the store has updated, it pushes the new state back to the view.

The Flux pattern helps us avoid the problems that provide and inject create and is the reason why Vuex is based on it.

note Even though Vuex is primarily Flux-like, it also takes some inspiration from Elm's MVU Architecture .

How to manually install and set up Vuex

To manually install and set up Vuex, we follow a simple 2 step process.

  • Step 1: Install the vuex package with npm
  • Step 3: Create the store

Step 1: Install the Vuex package with npm

Vuex isn’t included in the Vue package by default. You can install it with npm by executing the following command in your project folder.

tip If you’re working in VSCode, you can go to Terminal > New Terminal to open a terminal or command prompt with the project folder already selected.

Command: install with npm
npm install vuex@next --save

Vuex is a runtime dependency so we use the --save flag instead of the --saveDev flag.

Alternatively, you can use yarn.

Command: install with yarn
yarn add vuex@next --save

Once the installation is complete, you should see Vuex in the list of dependencies in your package.json file.

Example: package.json
"dependencies": {
  "core-js": "^3.6.5",
  "vue": "^3.0.0",
  "vue-router": "^4.0.10",
  "vuex": "^4.0.2"
}

Step 2: Create the store

Every Vuex application has one “store”, which is essentially just a container that holds the application’s state. It’s similar to a global object, but with a couple of important differences.

  • A Vuex store is reactive. It will reactively update the store’s state when a component retrieves from it.
  • A store’s state cannot be mutated directly. It must be done explicitly, which ensures that every state has a record and can be tracked.

Creating a store is similar to creating a router .

1. We start by opening the /src/main.js file and importing the createStore method from the ‘vuex’ package.

Example: src/main.js
import { createApp } from 'vue'
import App from './App.vue'

// import the createStore method
import { createStore } from 'vuex'

const app = createApp(App)

app.mount('#app')

2. The next step is to actually create the store with the imported method.

The createStore method takes a configuration object as an argument with at least one option.

  • state method

    This method contains the entire state of the application and returns a state object in the special $store.state instance variable.

tip The state method acts like a global data method we usually set up in a component’s config object.

Example: src/main.js
import { createApp } from 'vue'
import App from './App.vue'

// import the createStore method
import { createStore } from 'vuex'

// configure the store
const store = createStore({
  state() {
    return {

    }
  }
})

const app = createApp(App)

app.mount('#app')

3. The final step is to use the newly created store in the app before it is mounted.

Example: src/main.js
import { createApp } from 'vue'
import App from './App.vue'

// import the createStore method
import { createStore } from 'vuex'

// configure the store
const store = createStore({
  state() {
    return {

    }
  }
})

const app = createApp(App)

// use the store in the App
app.use(store)

app.mount('#app')

Now we’re set up and ready to start using the store.

How to use the Vuex store

As mentioned before, the state method is a global version of the data method. So, we can add any data properties to it that we want to be available to the entire application.

As an example, let’s create a simple counter property with an initial value of 0.

Example: src/main.js
import { createApp   } from 'vue'
import { createStore } from 'vuex'
import App from './App.vue'

const store = createStore({
  state() {
    return {
      counter: 0
    }
  }
})

const app = createApp(App)

app.use(store)
app.mount('#app')

That property can then be accessed in any component in our app by using the special $store.state instance variable.

As an example, we’ll create a method in the root App component that increments the counter property when the user clicks a button.

Example: src/App.vue
<template>
  <p>{{ $store.state.counter }}</p>
  <button @click="increment">Increment</button>
</template>

<script>
export default {
  methods: {
    increment() {
      this.$store.state.counter++
    }
  }
}
</script>

If we go to the browser and click on the button, the counter increments. We didn’t have to inject anything or pass props to the root App component.

To expand on the example, we can create a component in /src/components/IncrementCounter.vue that is responsible only for incrementing the counter.

Example: project
project_folder/
├── src/
|   ├── components/
|   |   └── IncrementCounter.vue
|   └── App.vue
Example: src/components/IncrementCounter.vue
<template>
  <button @click="increment">Increment</button>
</template>

<script>
export default {
  methods: {
    increment() {
      this.$store.state.counter++
    }
  }
}
</script>

Then we replace the button in the root App component template with the IncrementCounter component.

Example: src/App.vue
<template>
  <p>{{ $store.state.counter }}</p>
  <increment-counter />
</template>

<script>
import IncrementCounter from './components/IncrementCounter'

export default {
  components: {
    IncrementCounter
  }
}
</script>

If we go to the browser and click on the button, everything still works as expected.

note Store properties shouldn’t be used to replace any props we pass to a component’s children. A good rule of thumb is to only use them where you would normally use provide and inject.

How to change data with Mutations

At the moment, we’re directly communicating with our components, which isn’t ideal. We can still introduce mistakes and unexpected behavior into our application.

As an example, let’s consider that we want to increment the counter in another component as well, but with a different mechanism, like a mouseover. If we ever have to change the way the counter is incremented, we have to do it in multiple places, which can lead to mistakes or code duplication.

The ideal way would be to have the increment method in a single place, then allow any component to implement it.

Vuex allows us to define such methods in the mutations option of the store, which is the global equivalent of the methods option we usually define in a component’s config object.

Any method we define will automatically receive the state object as a parameter. We can use it instead of this to reference the data from the state method.

Let’s see it in action by moving our increment method from earlier to the store.

Example: src/main.js
import { createApp   } from 'vue'
import { createStore } from 'vuex'
import App from './App.vue'

const store = createStore({
  state() {
    return {
      counter: 0
    }
  },
  mutations: {
    increment(state) {
      state.counter++
    }
  }
})

const app = createApp(App)

app.use(store)
app.mount('#app')

To access the mutation in a component, we specify its name as a string argument in the special $store.commit method.

We can use it directly in the template block.

Example: src/components/IncrementCounter.vue
<template>
  <button @click="$store.commit('increment')">Increment</button>
</template>

Or through a method defined in the config object

Example: src/components/IncrementCounter.vue
<template>
  <button @click="increment">Increment</button>
</template>

<script>
export default {
  methods: {
    increment() {
      this.$store.commit('increment')
    }
  }
}
</script>

If we use any of the two examples above and click the button in the browser, the counter will increment.

If we ever need to modify the incrementing behavior, we only have to do it in one place.

note All mutations must be synchronous. If we need to perform an asynchronous task, we use Actions before invoking mutations.

How to pass data to a mutation with payloads

We want to keep state changes in a single place as much as possible, but sometimes we may need to pass data to a Mutation method.

Vuex allows us to do that with a payload parameter, which is essentially the same as a regular parameter.

Syntax: payload
mutations: {
  increment(state, payload) {
    // use payload
  }
}

As an example, let’s increment our number with a value that’s passed to the payload .

Example: src/main.js
import { createApp   } from 'vue'
import { createStore } from 'vuex'
import App from './App.vue'

const store = createStore({
  state() {
    return {
      counter: 0
    }
  },
  mutations: {
    increment(state, payload) {
      state.counter = state.counter + payload
    }
  }
})

const app = createApp(App)

app.use(store)
app.mount('#app')

The mutation method call will now be able to accept a second argument. Our mutation expects a number, so we’ll pass in 10.

Example: src/components/IncrementCounter.vue
<template>
  <button @click="increment">Increment</button>
</template>

<script>
export default {
  methods: {
    increment() {
      this.$store.commit('increment', 10)
    }
  }
}
</script>

If we go to the browser and click the button, the number will increment by 10.

How to retrieve data with Getters

Just as we don’t directly change the state from outside the store, we shouldn’t directly retrieve data from the state.

Vuex allows us to fetch data indirectly with getter methods in the getters option of the store. Any method we define will also automatically receive the state object as the first parameter.

tip Getter methods are conceptually the same as Class Accessors in Javascript (and many other general programming languages).

Example: src/main.js
import { createApp   } from 'vue'
import { createStore } from 'vuex'
import App from './App.vue'

const store = createStore({
  state() {
    return {
      counter: 0
    }
  },
  mutations: {
    increment(state, payload) {
      state.counter = state.counter + payload
    }
  },
  getters: {
    getCounter(state) {
      return state.counter
    }
  }
})

const app = createApp(App)

app.use(store)
app.mount('#app')

We can then reference the getter method through the special $store.getters instance variable. And like mutations, that can be either directly in a template block, or through a method/computed property in a config object.

Example: src/App.vue
<template>
  <p>{{ $store.getters.getCounter }}</p>
  <increment-counter />
</template>

<script>
import IncrementCounter from './components/IncrementCounter'

export default {
  components: {
    IncrementCounter
  }
}
</script>

Getters object

Getter methods can also automatically receive an optional second parameter, the getters object.

The object gives us access to all the other getter methods in the getters option, allowing us to use their returned values in our logic.

As an example, let’s say we have a second getter method that limits the counter value to 50. So we can use the getter object to get the output from the getCounter getter method.

Example: src/main.js
import { createApp   } from 'vue'
import { createStore } from 'vuex'
import App from './App.vue'

const store = createStore({
  state() {
    return {
      counter: 0
    }
  },
  mutations: {
    increment(state, payload) {
      state.counter = state.counter + payload
    }
  },
  getters: {
    getCounter(state) {
      return state.counter
    },
    getNormalizedCounter(state, getter) {
      // get the getCounter() return
      // value from the object
      if (getter.getCounter >= 50) {
        return 50
      }
      return getter.getCounter
    }
  }
})

const app = createApp(App)

app.use(store)
app.mount('#app')

note Because it doesn’t have a default value, we will always have to pass the state parameter to the method first, even if we don’t use it.

To help with the demonstration, we’ll also add the new method to where we output it in root App’s template.

Example: src/App.vue
<template>
  <p>Counter: {{ $store.getters.getCounter }}</p>
  <p>Normalized: {{ $store.getters.getNormalizedCounter }}</p>
  <increment-counter />
</template>

<script>
import IncrementCounter from './components/IncrementCounter'

export default {
  components: {
    IncrementCounter
  }
}
</script>

If we go to the browser and click on the button, the normalized counter will stop when it reaches 50.

Async operations with Actions

As mentioned earlier, mutations must always be synchronous. It should only ever be able to update the latest state.

If we run asynchronous code in a mutation, another mutation could change the data before the async operation completes and give it the wrong data.

Actions allow us to perform async operations before a mutation is invoked.

An action is just a method in the actions option of the store. It will automatically receive the context parameter, which also has a commit method that we use to invoke a mutation.

If the mutation receives a payload, it’s specified when we commit the mutation in the action method.

tip We can, and should, name our action methods the same as the mutation they are invoking. It makes it easier to see at a glance which action is connected to a mutation.

Example: src/main.js
import { createApp   } from 'vue'
import { createStore } from 'vuex'
import App from './App.vue'

const store = createStore({
  state() {
    return {
      counter: 0
    }
  },
  getters: {
    getCounter(state) { return state.counter }
  },
  mutations: {
    increment(state, payload) {
      state.counter = state.counter + payload
    }
  },
  actions: {
    increment(context) {
      // commit 'increment' mutation
      // with a payload
      context.commit('increment', 10);
    }
  }
})

const app = createApp(App)

app.use(store)
app.mount('#app')

To invoke an action, we use the dispatch method. The method accepts the action name as a string argument, and any payload we need to pass to the mutation as an optional second argument.

Example: src/App.vue
<template>
  <p>Counter: {{ $store.getters.getCounter }}</p>
  <button @click="incrementCounter">Increment</button>
</template>

<script>
export default {
  methods: {
    incrementCounter() {
      this.$store.dispatch('increment')
    }
  }
}
</script>

If we go to the browser and click on the button, everything should work as expected and the counter will increment by 10.

As mentioned earlier, actions are where we perform async operations before comitting a mutation.

A real world example would be making an HTTP request to an API, but to keep the demonstration simple we’ll just use a Javascript timer.

Example: src/main.js
import { createApp   } from 'vue'
import { createStore } from 'vuex'
import App from './App.vue'

const store = createStore({
  state() {
    return {
      counter: 0
    }
  },
  getters: {
    getCounter(state) { return state.counter }
  },
  mutations: {
    increment(state, payload) {
      state.counter = state.counter + payload
    }
  },
  actions: {
    increment(context) {

      // commit the 'increment'
      // mutation after 2 seconds
      setTimeout(() => {
        context.commit('increment', 10);
      }, 2000)
    }
  }
})

const app = createApp(App)

app.use(store)
app.mount('#app')

This time when we click the button in the browser, the counter will only increment after 2 seconds.

How to forward a payload

An action will automatically receive the payload parameter as an optional second parameter.

We can forward that payload so that we’ll be able to accept it in the dispatch method where the action is invoked.

To demonstrate, we will add the payload parameter to our action where we previously specified a value.

Example: src/main.js
import { createApp   } from 'vue'
import { createStore } from 'vuex'
import App from './App.vue'

const store = createStore({
  state() {
    return {
      counter: 0
    }
  },
  getters: {
    getCounter(state) { return state.counter }
  },
  mutations: {
    increment(state, payload) {
      state.counter = state.counter + payload
    }
  },
  actions: {
    // automatically get 'payload' param
    increment(context, payload) {

      // forward payload to 'dispatch' method
      context.commit('increment', payload);
    }
  }
})

const app = createApp(App)

app.use(store)
app.mount('#app')

The dispatch method will then accept a value for the payload as second parameter.

Example: src/App.js
<template>
  <p>Counter: {{ $store.getters.getCounter }}</p>
  <button @click="incrementCounter">Increment</button>
</template>

<script>
export default {
  methods: {
    incrementCounter() {
      // specify value for payload
      this.$store.dispatch('increment', 5)
    }
  }
}
</script>

If we run the example in the browser and click on the button, the counter will increment by 5.

Mapper convenience methods

Now that we know the core features of the Vuex library, let’s discuss some of the utility features it provides.

Vuex makes our lives a little easier by providing us with two convenience methods.

  • mapGetters
  • mapActions

These methods automatically map getters and actions so that we don’t have to create them manually.

How to map getters with mapGetters

At the moment, we output our counter property by tapping directly into the store.

Example: src/App.vue
<template>
  <p>Counter: {{ $store.getters.getCounter }}</p>
</template>

A more ideal way to do this would be to use a computed property .

Example: src/App.vue
<template>
  <p>Counter: {{ trackCounter }}</p>
  <button @click="incrementCounter">Increment</button>
</template>

<script>
export default {
  methods: {
    incrementCounter() {
      this.$store.dispatch('increment', 5)
    }
  },
  computed: {
    trackCounter() {
      return this.$store.getters.getCounter
    }
  }
}
</script>

Instead of creating a computed property for each getter we have, Vuex can do it for us with the mapGetters method. This method will automatically define our getters as computed properties and return them as an object.

To use the method we import it from the ‘vuex’ package, then spread it in the computed option with Javascript spread syntax .

To ensure we don’t have unnecessary getters, the method takes an array of getters we want to turn into computed properties as an argument.

Example: src/App.vue
<template>
  <p>Counter: {{ getCounter }}</p>
  <button @click="incrementCounter">Increment</button>
</template>

<script>
import { mapGetters } from 'vuex'

export default {
  methods: {
    incrementCounter() {
      this.$store.dispatch('increment', 5)
    }
  },
  computed: {
    // convert the 'getCounter' getter
    // into a computed property with the
    // same name
    ...mapGetters(['getCounter'])
  }
}
</script>

If we go to the browser and click on the button, everything will still work as expected.

tip Mappers save us a lot of time and code, and ensures we don’t make any mistakes when defining our computed properties.

How to map actions with mapActions

At the moment we dispatch our increment action through the incrementCounter method in the methods option. Similar to getters, Vuex allows us to map our actions with the mapActions method.

It works the same as the mapGetters method, but we spread mapActions in the methods option instead.

Example: src/App.vue
<template>
  <p>Counter: {{ getCounter }}</p>
  <button @click="increment">Increment</button>
</template>

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

export default {
  methods: {
    // convert the 'increment' action
    // into a method with the same name
    ...mapActions(['increment'])
  },
  computed: {
    ...mapGetters(['getCounter'])
  }
}
</script>

The example above won’t work, because our action requires a payload. If the action requires a payload, we specify it where we invoke the action.

In our case that would be the click handler on the button.

Example: src/App.vue
<template>
  <p>Counter: {{ getCounter }}</p>
  <button @click="increment(3)">Increment</button>
</template>

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

export default {
  methods: {
    // convert the 'increment' action
    // into a method with the same name
    ...mapActions(['increment'])
  },
  computed: {
    ...mapGetters(['getCounter'])
  }
}
</script>

If we go to the browser and click on the button, the counter will increment by 3. So, everything still works as expected but with less code and fewer chances of making mistakes.

Alternative mapper syntax

While the same names make connections easier to see at a glance, Vuex does allow us to specify different names for our actions and getters with an alternative syntax.

Instead of an array, we specify key:value pairs in an object, where the key is the new name and the value is the action or getter.

Syntax: alternative syntax
...mapActions({
  new_action_name: 'action'
})

// and
...mapGetters({
  new_getter_name: 'getter'
})

To demonstrate, let’s modify our example with different names for both the action and the getter.

Example: src/App.vue
<template>
  <p>Counter: {{ trackCounter }}</p>
  <button @click="incrementCounter(7)">Increment</button>
</template>

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

export default {
  methods: {
    ...mapActions({
      // the 'increment' action is now
      // a method called 'incrementCounter'
      incrementCounter: 'increment'
    })
  },
  computed: {
    ...mapGetters({
      // the 'getCounter' getter is now a
      // computed property called 'trackCounter'
      trackCounter: 'getCounter'
    })
  }
}
</script>

If we run the example in the browser, everything will still work as expected.

How to scaffold a new project with Vuex

The Vue CLI allows us to add the Vuex Package when we scaffold a new project.

We want to Manually select features.

Example: manual selection
? Please pick a preset:
  Default ([Vue 2] babel, eslint)
  Default (Vue 3) ([Vue 3] babel, eslint)
> Manually select features

Then add Vuex.

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

The CLI will set up a store in /src/store/index.js and register it in the application’s config.

Further Reading

For more information on the topics covered in this lesson, please see the relevant section below.