Vuex - What's it for?


Author: Rachel
January 3, 2020 | 5 mins to read

Every app works with data in some capacity. For simple apps, it's relatively easy to manage data, but for complex applications that require data in multiple components, it is easier to extract the data to a higher level. This is when a state management system is necessary. Vuex is a library for Vue.js that offers a global state management system. You'll likely see it for any app larger than a few web pages.

It can be compared to Flux and Redux for React, which provide a global singleton state that can be accessed by any component regardless of where they reside on the component tree. This addresses an issue seen with heavily nested components, leading to unmaintainable code that breaks when the component hierarchy changes (something I saw quite a lot of when working with Angular 1).

Vuex is also integrated with the official Vue.js devtools extension, which allows zero-config time-travel debugging and state snapshot export / import. Debugging a javascript application couldn't be easier.

The Store

Vuex is initialized by creating a Store, which maintains the entire application state at any given point in time. The store is reactive, forcing an update if a Vue component detects the store's state has changed. The store also cannot be directly changed unless an explicit mutation is called. This makes it clear that a state change is necessary due to an action committed within the application. It makes it easier to track every action and trace bugs as well as reduce the likelihood of new bugs.

A simple store can be instantiated with the code below:

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    add (state) {
      state.count++
    }
  }
})

In this example, the store is created with a state and a mutation. The state variable of count is created, along with a mutation for manipulating count. This value can now be accessed within Vue using $store.state.count. count can also be increased by committing a mutation using store.commit('add'). This makes it easy to log state changes, which is useful for troubleshooting unexpected behavior.

In addition to state and mutations, getters and actions also make up the Store. Let's take a look.

The State

The state is maintains all application variables, and any component can access the state and retrieve a value.

To enable it to be accessed by any application component, the Store must be injected into the application root:

const app = new Vue({
  el: '#app',
  // provide the store using the "store" option.
  // this will inject the store instance to all child components.
  store,
  components: { Counter },
  template: `
    <div class="app">
      <counter></counter>
    </div>
  `
})

The count value can now be accessed using a Component's computed method:

computed: {
    count () {
      return this.$store.state.count
    }
  }

By using a computed method, any changes to the count value will automatically trigger a DOM update.

It's also worth noting that not every variable needs to be in the Vuex Store. Some variables are only needed locally, in which case it's best to maintain it as part of the component's local state.

Getters

Just as a local store may use computed properties to calculate a new value based on an existing value, the same is true for Vuex store state values. Getters handle this scenario, which are exposed with store.getters.

getters: {
    doneTodos: state => {
      return state.todos.filter(todo => todo.done)
    }
  }

In the above example, the list of To-Dos that are complete are filtered from all todos. The doneTodos getter returns only the todos that are marked done.

This can now be accessed by any component when accessing the store:

computed: {
  doneTodosCount () {
    return this.$store.getters.doneTodosCount
  }
}

The doneTodosCount computed property returns the total number of Todos that are marked complete, computed by the store.

Mutations

To update or modify a value in the Store's state, mutations are needed. Mutations modify the state and return the resulting state:

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

In the above example, the add mutation updates the state by the payload amount.

The mutation above can be triggered by using commit:

let payload = {
  amount: 20
}
store.commit('add', payload)

A few important notes regarding mutations:

  • If you need to add new properties to the state, it's important to either use a new Object or use the Vue.set method to update the object. Otherwise, changes may go unnoticed and the DOM may not update.
  • Mutations must be synchronous. For asynchronous operations, that's where actions come in.

Actions

Actions are similar to mutations, except they can handle asynchronous operations, such as API calls. Actions also allow committing mutations.

Let's take a look at an action:

actions: {
  add ({ commit }) {
    commit('add')
  }
}

Each action handler is provided a context object, which allows access to the store's state, getters, mutations via commit, and other actions, via dispatch. The above action passes in commit, allowing mutations to be committed to the state.

To call the add action, we use store.dispatch('add'). Dispatch can handle Promises, which enables complex async workflows, chaining multiple actions together if necessary.

actions: {
  async updateProfile ({ commit }) {
    commit('setUser', await updateUser())
  },
  async displayDashboard ({ dispatch, commit }) {
    await dispatch('updateProfile') // wait for `actionA` to finish
    commit('showDashboard', await getDashboardData())
  }
}

Helpers

To use the Store state, getters, mutations, and actions within components helpers are available with mapState, mapGetters, mapMutations, and mapActions. These helpers map store state, getters, mutations, and actions to the local equivalent.

This is useful for keeping code concise and easy to read, bringing in just what you need for that component:

computed: {
    ...mapGetters([
      'firstGetter',
      'anotherGetter',
    ])
  }

Modules

As an application grows, the store grows. To keep it manageable, Vuex allows breaking up the Store into modules. This is great for grouping features together.

const cartModule = {
  state: { ... },
  actions: { ... }
}

const userModule = {
  state: { ... },
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  modules: {
    cart: cartModule,
    user: userModule
  }
})

store.state.cart
store.state.user

By default, all modules are registered as part of the global namespace, so cartModule can access everything in userModule and vice versa.

To provide further isolation though, modules can be namespaced with namespace: true.

To access global state and getters within namespaced modules, the rootState and rootGetters are passed into the function.

  someGetter (state, getters, rootState, rootGetters) {... }

Namespacing modules is quite powerful, and I would recommend beginning projects with this in mind rather than as an afterthought. It helps with code maintainability and keeps other developers on the team happy. It's no fun starting on a new project with 10k+ lines of code. Write modular code. It'll save many headaches down the road.

For more details on how modules can promote reusable code and self-contained functionality, visit Vuex Modules.

A powerful state management system

Vuex is a powerful state management system and is will be necessary for complex web applications. It's easy to understand, but hard to master. Do you have a use case in mind?