Vuex, state management for VueJs in detail

Alex Maher
14 min readJan 6, 2023

--

Why do we need state management?

As web applications grow in size and complexity, it becomes increasingly difficult to manage the state of different components and keep them in sync. This is where state management patterns like Vuex come in handy.

But why do we need state management in the first place?

Complexity

As the application grows, the number of components and the interactions between them increase, making it harder to understand and maintain the code. State management helps to centralize the application state and provide a single source of truth, making the code more predictable and easier to understand.

Reusability

Without state management, it becomes difficult to reuse components as they are tightly coupled to their local state. State management allows us to decouple components from their local state and make them more reusable.

Performance

In large applications, updating the state in one place and having it reflected everywhere else can be a performance bottleneck. State management helps to optimize the rendering of components by reducing the number of unnecessary updates.

Maintainability

Without state management, the application state is scattered across different components, making it harder to track and maintain. State management helps to centralize the application state and make it easier to track and debug.

In summary, state management is essential for building scalable and maintainable web applications. It helps to centralize the application state, optimize performance, improve reusability, and make the code easier to understand and maintain.

What is Vuex?

Vuex is a state management pattern and library for Vue.js that helps us manage the state of our application in a central store, and provides methods for accessing and mutating the state in a consistent and predictable way.

Vuex was inspired by the Flux and Redux architectures, and follows a similar pattern of unidirectional data flow. It provides a simple and scalable solution for managing the state of large Vue.js applications.

Core concepts

Vuex has a few core concepts that are important to understand:

  • State: In Vuex, the state is an object that holds the application data. It is the single source of truth for the application. The state is read-only, and the only way to mutate the state is by committing mutations.
  • Getters: Getters are like computed properties for the store. They allow us to access the state in a derived and reusable way.
  • Mutations: Mutations are the only way to mutate the state in a Vuex store. They are like event handlers that are called with a payload to make changes to the state. Mutations must be synchronous and should be committed using the commit method.
  • Actions: Actions are like asynchronous mutations. They are functions that commit mutations, and can contain any arbitrary logic. Actions can be asynchronous, and are useful for making API calls or performing complex operations.
  • Modules: Vuex stores can be organized into modules to keep the code clean and organized. Each module can have its own state, getters, mutations, and actions.

Benefits

Vuex has a few benefits that make it a great choice for state management in Vue.js applications:

  • It provides a central place for storing and managing the state of the application, making the code more predictable and easier to understand.
  • It follows a unidirectional data flow, making the application easier to debug and maintain.
  • It optimizes the rendering of components by reducing the number of unnecessary updates.
  • It makes it easy to reuse components by decoupling them from their local state.

Setting up Vuex in a Vue.js application

To set up Vuex in a Vue.js application, we need to install the Vuex library and create a store file.

To install Vuex, we can use npm or yarn to add it as a dependency.

Here is the command to install Vuex using npm:

npm install vuex

Here is the command to install Vuex using yarn:

yarn add vuex

To create a store file, we can create a new file called store.js in the root of our project and add the Vuex store code.

Here is an example of a Vuex store with a simple count state that increments when a button is clicked:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)
export default new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
}
})

Injecting the store into the Vue instance

To inject the store into the Vue instance, we need to import the store file and pass it to the store option of the Vue instance.

In the entry point of our application (e.g. main.js), we can import the store and pass it to the store option of the Vue instance:

import Vue from 'vue'
import store from './store'

new Vue({
el: '#app',
store,
template: `
<div>
<button @click="increment">{{ count }}</button>
</div>
`,
computed: {
count () {
return this.$store.state.count
}
},
methods: {
increment () {
this.$store.commit('increment')
}
}
})

In a Vue component, we can access the store using the $store property:

<template>
<div>
<button @click="increment">{{ count }}</button>
</div>
</template>

<script>
export default {
name: 'Counter',
computed: {
count () {
return this.$store.state.count
}
},
methods: {
increment () {
this.$store.commit('increment')
}
}
}
</script>

Best practices

Here are a few best practices for setting up Vuex in a Vue.js application:

  • Install Vuex as a dependency: Use npm or yarn to install Vuex as a dependency of your project.
  • Create a store file: Create a store file to define the state, mutations, actions, and getters of your application.
  • Inject the store into the Vue instance: Import the store file and pass it to the store option of the Vue instance.
  • Access the store in Vue components: Use the $store property to access the store in Vue components.

Vuex state

In Vuex, the state is an object that holds the application data. It is the single source of truth for the application, and is the central place for storing and managing the state of the application.

The state is read-only, and the only way to mutate the state is by committing mutations. This ensures that the state can only be changed in a predictable and consistent way, making the application easier to debug and maintain.

Defining the state

To define the state in a Vuex store, we need to create a state object and assign it to the state property of the store. Here is an example of a store with a simple count state:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)
export default new Vuex.Store({
state: {
count: 0
}
})

Reading the state

To read the state in a Vue component, we need to inject the store using the store option. We can then access the state using the $store.state object. Here is an example of a component that displays the count:

<template>
<div>
<p>Count: {{ $store.state.count }}</p>
</div>
</template>

<script>
export default {
name: 'Counter',
computed: {
count () {
return this.$store.state.count
}
}
}
</script>

We can also use a mapState helper function to map the state to computed properties. This helps to keep the template clean and makes the code more readable. Here is an example of the same component using the mapState helper:

<template>
<div>
<p>Count: {{ count }}</p>
</div>
</template>

<script>
import { mapState } from 'vuex'
export default {
name: 'Counter',
computed: {
...mapState(['count'])
}
}
</script>

Best practices

Here are a few best practices for using Vuex state:

  • Keep the state minimal: Avoid storing data that can be computed from the state or can be fetched from an API.
  • Use getters for derived state: Use getters to avoid duplicating derived state in multiple components.
  • Avoid direct state manipulation: Avoid directly mutating the state or using the state as a temporary storage. Use mutations and actions to make changes to the state.

Vuex Getters

In Vuex, getters are like computed properties for the store. They allow us to access the state in a derived and reusable way.

Getters are useful for abstracting away complex logic or for exposing a computed value from the state. They are also reactive, which means that they will update whenever the state changes.

Defining getters

To define getters in a Vuex store, we need to create a getters object and assign it to the getters property of the store. Each getter is a function that takes the state as an argument and returns a derived value.

Here is an example of a store with a simple doubleCount getter that returns the double of the count state:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)
export default new Vuex.Store({
state: {
count: 0
},
getters: {
doubleCount: state => state.count * 2
}
})

Reading getters

To read getters in a Vue component, we need to inject the store using the store option. We can then access the getters using the $store.getters object. Here is an example of a component that displays the double count:

<template>
<div>
<p>Double Count: {{ $store.getters.doubleCount }}</p>
</div>
</template>

<script>
export default {
name: 'Counter',
computed: {
doubleCount () {
return this.$store.getters.doubleCount
}
}
}
</script>

We can also use a mapGetters helper function to map the getters to computed properties. This helps to keep the template clean and makes the code more readable. Here is an example of the same component using the mapGetters helper:

<template>
<div>
<p>Double Count: {{ doubleCount }}</p>
</div>
</template>

<script>
import { mapGetters } from 'vuex'
export default {
name: 'Counter',
computed: {
...mapGetters(['doubleCount'])
}
}
</script>

Best practices

Here are a few best practices for using Vuex getters:

  • Avoid duplicating derived state: Use getters to avoid duplicating derived state in multiple components.
  • Keep the logic in the getters: Avoid complex logic in the template and keep it in the getters.
  • Use getters for derived state: Use getters to avoid recomputing derived state every time it is needed.

Vuex Mutations

In Vuex, mutations are the only way to mutate the state in a store. They are like event handlers that are called with a payload to make changes to the state.

Mutations must be synchronous and should be committed using the commit method. This ensures that the state can only be changed in a predictable and consistent way, making the application easier to debug and maintain.

Defining mutations

To define mutations in a Vuex store, we need to create a mutations object and assign it to the mutations property of the store. Each mutation is a function that takes the state and a payload as arguments and modifies the state.

Here is an example of a store with a simple increment mutation that increments the count state:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)
export default new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
}
})

Committing mutations

To commit a mutation in a Vue component, we need to use the commit method. The commit method takes the mutation type and an optional payload as arguments.

Here is an example of a component that commits the increment mutation when a button is clicked:

<template>
<div>
<button @click="increment">Increment</button>
</div>
</template>

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

Best practices

Here are a few best practices for using Vuex mutations:

  • Keep the mutations simple: Avoid complex logic in the mutations and keep them simple and focused.
  • Use mutations for direct state changes: Use mutations to make direct changes to the state. Avoid making API calls or performing complex operations in mutations.
  • Use constants for mutation types: Use constants for mutation types to avoid typos and make the code more predictable.

Vuex Actions

In Vuex, actions are like asynchronous mutations. They are functions that commit mutations, and can contain any arbitrary logic. Actions can be asynchronous, and are useful for making API calls or performing complex operations.

Defining actions

To define actions in a Vuex store, we need to create an actions object and assign it to the actions property of the store. Each action is a function that takes a context object and a payload as arguments, and can contain any arbitrary logic.

The context object has the same properties as the store, and allows us to access the state, getters, and commit mutations.

Here is an example of a store with a simple incrementAsync action that increments the count state after a delay:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)
export default new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
incrementAsync ({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
}
})

Dispatching actions

To dispatch an action in a Vue component, we need to use the dispatch method. The dispatch method takes the action type and an optional payload as arguments.

Here is an example of a component that dispatches the incrementAsync action when a button is clicked:

<template>
<div>
<button @click="incrementAsync">Increment Async</button>
</div>
</template>

<script>
export default {
name: 'Counter',
methods: {
incrementAsync () {
this.$store.dispatch('incrementAsync')
}
}
}
</script>

Best practices

Here are a few best practices for using Vuex actions:

  • Use actions for complex logic: Use actions to make API calls or perform complex operations. Avoid complex logic in the mutations and keep them simple and focused.
  • Use actions for async logic: Use actions to handle async logic and commit mutations when the async operations are complete.
  • Use constants for action types: Use constants for action types to avoid typos and make the code more predictable.
  • Return a promise from actions: Return a promise from actions to allow for async handling and to chain actions.
  • Avoid committing mutations directly in actions: Use the commit method from the context object to commit mutations in actions, rather than committing them directly. This allows for better debugging and maintainability.

Vuex Modules

In Vuex, modules are a way to divide the store into smaller, reusable pieces. Each module can have its own state, mutations, actions, and getters, and can be combined in a single store to create a larger, more complex application.

Defining modules

To define a module in a Vuex store, we need to create an object with the module’s properties and register it using the store.registerModule method. The module object can contain the following properties:

  • state: The module's state
  • mutations: The module's mutations
  • actions: The module's actions
  • getters: The module's getters
  • namespaced: A boolean flag that enables namespacing for the module.

Here is an example of a store with a simple counter module that increments a count state:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)
const counterModule = {
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
incrementAsync ({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
},
getters: {
doubleCount: state => state.count * 2
},
namespaced: true
}
const store = new Vuex.Store({
modules: {
counter: counterModule
}
})
store.registerModule('counter', counterModule)

Reading module state

To read the state of a module in a Vue component, we can use the mapState helper function or access the module's state directly.

If the module is namespaced, we need to pass the module name as a namespace argument to the mapState function.

Here is an example of a component that reads the count state from the counter module using the mapState helper function:

<template>
<div>
<p>Count: {{ count }}</p>
</div>
</template>

<script>
export default {
name: 'Counter',
computed: {
...mapState('counter', ['count'])
}
}
</script>

Here is an example of a component that reads the count state from the counter module directly:

<template>
<div>
<p>Count: {{ $store.state.counter.count }}</p>
</div>
</template>

<script>
export default {
name: 'Counter'
}
</script>

Committing module mutations

To commit a mutation in a module, we need to use the commit method and pass the module name as a namespace argument.

Here is an example of a component that commits the increment mutation in the counter module:

<template>
<div>
<button @click="increment">Increment</button>
</div>
</template>

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

Dispatching module actions

To dispatch an action in a module, we need to use the dispatch method and pass the module name as a namespace argument.

Here is an example of a component that dispatches the incrementAsync action in the counter module:

<template>
<div>
<button @click="incrementAsync">Increment Async</button>
</div>
</template>

<script>
export default {
name: 'Counter',
methods: {
incrementAsync () {
this.$store.dispatch('counter/incrementAsync')
}
}
}
</script>

Best practices

Here are a few best practices for using Vuex modules:

  • Use modules for reusable, self-contained logic: Use modules to divide the store into reusable, self-contained pieces.
  • Use namespacing for modules: Use namespacing for modules to avoid naming collisions and make the code more predictable.
  • Keep modules small and focused: Avoid creating large, complex modules. Instead, create smaller, focused modules that can be combined to create a larger application.
  • Use namespaced constants for module types: Use namespaced constants for module types to avoid typos and make the code more predictable.

Complex Examples

Mutations in Vuex can be as simple or as complex as needed.

Here is an example of a Vuex store with a complex mutation that updates multiple properties of the user state:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)
export default new Vuex.Store({
state: {
user: {
id: 1,
name: 'John',
email: 'john@example.com'
}
},
mutations: {
updateUser (state, payload) {
state.user.id = payload.id
state.user.name = payload.name
state.user.email = payload.email
}
}
})

In a Vue component, we can commit the updateUser mutation using a method:

<template>
<div>
<button @click="updateUser">Update user</button>
</div>
</template>

<script>
export default {
name: 'User',
methods: {
updateUser () {
this.$store.commit('updateUser', {
id: 2,
name: 'Jane',
email: 'jane@example.com'
})
}
}
}
</script>

Here is an example of a Vuex store with a complex action that fetches data from an API and commits a mutation:

import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'

Vue.use(Vuex)
export default new Vuex.Store({
state: {
user: {
id: null,
name: '',
email: ''
}
},
mutations: {
setUser (state, payload) {
state.user.id = payload.id
state.user.name = payload.name
state.user.email = payload.email
}
},
actions: {
async fetchUser ({ commit }) {
const response = await axios.get('/api/user')
commit('setUser', response.data)
}
}
})

In a Vue component, we can dispatch the fetchUser action using a method:

<template>
<div>
<button @click="fetchUser">Fetch user</button>
</div>
</template>

<script>
export default {
name: 'User',
methods: {
async fetchUser () {
await this.$store.dispatch('fetchUser')
}
}
}
</script>

Here is an example of a Vuex store with a very complicated app state that includes multiple nested properties:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)
export default new Vuex.Store({
state: {
app: {
user: {
id: 1,
name: 'John',
email: 'john@example.com'
},
settings: {
theme: 'light',
language: 'en'
},
data: {
items: [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' }
]
}
}
}
})

In a Vue component, we can access the app state using computed properties:

<template>
<div>
{{ app }}
</div>
</template>

<script>
export default {
name: 'App',
computed: {
app () {
return this.$store.state.app
}
}
}
</script>

Most Common Issues

Issue 1: Getting undefined when accessing the state

One of the most commonly hard to understand issues in Vuex is getting undefined when accessing the state.

There are a few possible causes for this issue:

  1. The store has not been injected into the Vue instance: Make sure to import the store file and pass it to the store option of the Vue instance.
  2. The state has not been initialized: Make sure to define the initial state in the store.
  3. The name of the state is incorrect: Make sure to use the correct name for the state.

Issue 2: Cannot mutate the state directly

Another commonly hard to understand issue in Vuex is trying to mutate the state directly.

In Vuex, the state is read-only. We need to commit a mutation to change the state.

Issue 3: Getting an error when dispatching an action

Another hard to understand issue in Vuex is getting an error when dispatching an action.

There are a few possible causes for this issue:

  1. The action is not defined in the store: Make sure to define the action in the store.
  2. The action is not being returned as a promise: Make sure to return a promise from the action.
  3. The action is not being handled correctly in the store: Make sure to handle the action correctly in the store.

These are some of the most commonly hard to understand issues in Vuex. By understanding these issues, we can avoid common pitfalls and write better Vuex code.

I hope you found this helpful and have a pretty good understanding of how Vuex works now. You can always come back and check things when you’re unsure. Make sure to bookmark :)

Thank you

--

--

Alex Maher
Alex Maher

Written by Alex Maher

.NET C# dev with 10+ yrs exp, self-taught & passionate web developer. Sharing tips & experiences in C# and web dev.

No responses yet