How To Manage State in a Vue.js Application with Vuex

The author selected Open Sourcing Mental Illness to receive a donation as part of the Write for DOnations program. Introduction Vuex is the first-party development state management library for Vue.js. It was created by Evan You and is currently maintained by the Vue.js Core Team. Like many other state management libraries, Vuex follows the principle […]

The author selected Open Sourcing Mental Illness to receive a donation as part of the Write for DOnations program.

Introduction

Vuex is the first-party development state management library for Vue.js. It was created by Evan You and is currently maintained by the Vue.js Core Team. Like many other state management libraries, Vuex follows the principle that Redux has popularized over the past years: Data flows in one direction, with actions and mutations modifying data in a single source of truth called the store.

A Vuex store is a collection of different methods and data. Some of these methods, such as actions, can fetch and process data before it is sent to a mutation. A mutation is a method that mutates or updates the store property with the value provided. Getters are methods that can modify or combine data to create a new state property. These getters are read-only and do not mutate data. These are similar to computed properties in a Vue.js component. The last component to Vuex is the state, or the dataset that acts as your single source of truth.

In this tutorial, you will create an application that renders a list of cards with airport information in them. When clicked, these cards will execute the Vuex workflow to add the selected airport to a list of favorites. By running through this example, you will make actions and mutations to manage state and getters to retrieve computed data.

Prerequisites

Step 1 — Setting Up the Example Application

In order to help visualize how state is managed with Vuex, set up a project with some data to display in the view. You will use this project and throughout the tutorial.

Once the favorite-airports project is created as described in the Prerequisites section, create a directory to hold all of your local data for this project. Open your terminal and run the following commands in the project root directory (favorite-airports):

  • mkdir src/data
  • touch src/data/airports.js

This will create the data directory and an empty airports.js file inside it.

In your text editor of choice, open the newly created airports.js file and add in the following:

favorite-airports/src/data/airports.js
export default [
  {
    name: 'Cincinnati/Northern Kentucky International Airport',
    abbreviation: 'CVG',
    city: 'Hebron',
    state: 'KY'
  },
  {
    name: 'Seattle-Tacoma International Airport',
    abbreviation: 'SEA',
    city: 'Seattle',
    state: 'WA',
  },
  {
    name: 'Minneapolis-Saint Paul International Airport',
    abbreviation: 'MSP',
    city: 'Bloomington',
    state: 'MN',
  },
  {
    name: 'Louis Armstrong New Orleans International Airport',
    abbreviation: 'MSY',
    city: 'New Orleans',
    state: 'LA',
  },
  {
    name: `Chicago O'hare International Airport`,
    abbreviation: 'ORD',
    city: 'Chicago',
    state: 'IL',
  },
  {
    name: `Miami International Airport`,
    abbreviation: 'MIA',
    city: 'Miami',
    state: 'FL',
  }
]

This is an array of objects consisting of a few airports in the United States. In this application, you are going to iterate through this data to generate cards consisting of the name, abbreviation, city, and state properties. When the user clicks on a card, you will execute a dispatch method, which will add that airport to your Vuex state as a favorite airport.

Save data/airports.js and return to the terminal.

When you’ve completed that step, create a single-file component (SFC) with the name AirportCard.vue. This file will live in the components directory of your project. This component will contain all the styles and logic for the airport card. In your terminal, create the .vue file using the touch command:

  • touch src/components/AirportCard.vue

Open AirportCard.vue in your text editor and add the following:

favorite-airports/src/components/AirportCard.vue
<template>
  <div class="airport">
    <p>{{ airport.abbreviation }}</p>
    <p>{{ airport.name }}</p>
    <p>{{ airport.city }}, {{ airport.state }}</p>
  </div>
</template>

<script>
export default {
  props: {
    airport: {
      type: Object,
      required: true
    }
  }
}
</script>

<style scoped>
.airport {
  border: 3px solid;
  border-radius: .5rem;
  padding: 1rem;
  margin-bottom: 1rem;
}

.airport p:first-child {
  font-weight: bold;
  font-size: 2.5rem;
  margin: 1rem 0;
}

.airport p:last-child {
  font-style: italic;
  font-size: .8rem;
}
</style>

You may notice that there is some CSS included in this code snippet. In the AirportCard.vue component, the wrapper <div> contains the class of airport. This CSS adds some styling to the generated HTML by adding borders to give each airport the appearance of a “card”. The :first-child and :last-child are pseudo selectors that apply different styling to the first and last p tags in the HTML inside the div with the class of airport. In addition to that, you may also notice that this component contains a prop, which in Vue.js is a way to pass data down from a parent component to a child component.

Save and exit from the file.

Before wrapping up the setup, replace the existing App.vue component with the following code:

favorite-airports/src/App.vue
<template>
  <div class="wrapper">
    <div v-for="airport in airports" :key="airport.abbreviation">
      <airport-card :airport="airport" />
    </div>
  </div>
</template>

<script>
import { ref } from 'vue'
import allAirports from '@/data/airports.js'
import AirportCard from '@/components/AirportCard.vue'

export default {
  components: {
    AirportCard
  },
  setup() {
    const airports = ref(allAirports)
    return { airports }
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}

.wrapper {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  grid-column-gap: 1rem;
  max-width: 960px;
  margin: 0 auto;
}

p,
h3 {
  grid-column: span 3;
}
</style>

This code contains a v-for loop that iterates through the airports.js data and renders a series of AirportCards.vue components with airport data passed in via the prop :airport. Save this code and return to the command line.

With the project set up, run the local development server using the npm run serve command in your terminal:

  • npm run serve

This will start a server on your localhost, usually at port 8080. Open your web browser of choice and visit localhost:8080to see the following:

A list of cards rendered in Vue using the v-for directive.

Now that your example application is set up, in the next step you are going to install the Vuex library and create a store. This store is a collection of a number of different Vuex items including: state, mutations, actions, and getters. To illustrate this, you will be executing dispatch methods, which will add an airport to a favorites section of your app.

Step 2 — Installing Vuex

When working on web-based applications, you will often work with state. State is a collection of data at a given time. This state can be changed with user interactions via dispatch and commit methods. When the user modifies data, a dispatch event is executed, which passes data to a mutation and updates the state object.

There are a few ways to approach updating state. Some developers skip actions and go straight to mutatations. However, for the sake of this tutorial, you will always execute an action that in turn calls a mutation. This way you can have multiple mutations inside of an action. The cardinal rule of Vuex is that mutations have one job and one job only: update the store. Actions can do a number of different things including combining data, fetching data, and running JavaScript logic.

In addition to actions, there are also getters. A getter is a way to combine multiple state values into a single value. If you are familiar with computed properties in Vue.js, getters can be thought of as state-specific computed properties.

With the Vuex terminology covered, start installing and integrating Vuex. Open your terminal and run the following command:

This command will install the version of Vuex that is the most compatible with Vue.js 3.x and saves it in your package.json file. Next, create a directory and an index file for your store. You will use the mkdir command to make a directory and touch to create a new file:

  • mkdir src/store
  • touch src/store/index.js

Open your text editor and in your store/index.js file, initialize your Vuex store. To do this, you need to leverage the createStore function from Vuex:

airport-favorites/src/store/index.js
import { createStore } from 'vuex'

export default createStore({

})

You also export this, as you will later import it into your main.js file.

At this point, you have your Vuex store set up, but the application is not yet aware of it or how to use it. To fully initialize the store, import it into your main.js file. In your text editor, open the src/main.js file.

Immediately after createApp(App), chain the use method and pass into the store that you are importing, as shown in the following highlighted code:

favorite-airports/src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import store from './store'

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

Once you’ve chained the use method, save this file. The use method tells the Vue application which code to bundle together when the application is built. In this case, you are telling Vue to “use” or bundle the Vuex store.

Before you move on to the next section, add a state value into your store and reference it in the App.vue file. Open your store/index.js file and add the following objects and values:

favorite-airports/src/store/index.js
import { createStore } from 'vuex'

export default createStore({
state: {
    firstName: 'John',
    lastName: 'Doe'
  },
mutations: {

},
actions: {

},
getters: {

}
})

These properties reflect the type of data the store holds: state for state (global data), mutations (commits that mutate data), actions (dispatches that call mutations), and getters (store computed properties).

Save store/index.js, then open your App.vue file in your text editor and add the following:

favorite-airports/src/App.vue
<template>
  <div class="wrapper">
    <p>{{ $store.state.firstName }} {{ $store.state.lastName }}</p>
    <div v-for="airport in airports" :key="airport.abbreviation">
      <airport-card :airport="airport" />
    </div>
  </div>
</template>
...

The $store in this case is the global store that you initialized in the main.js file. If you were to log this.$store into the console, you would see the store object. From there, the code accesses the property you want to display via dot notation.

Save App.vue then open your web browser. Above the airport cards, you will see the first and last name that you saved to your Vuex store. These are the default values of firstName and lastName, respectively.

Data from the Vuex store displayed in the view.

In this step, you installed Vuex and created a Vuex store. You added some default store data and displayed it in the view with the $store object using dot notion. In the next step, you will be updating your Vuex store via actions and mutations, and you will get combined data with getters.

Step 3 — Creating Actions, Mutations, and Getters

In Step 2, you installed Vuex manually and integrated it into your project. In this step you still have the first and last name rendered in your browser, but you will create a Vuex getter to render the data as one string. As mentioned before, you can think of Vuex getters as computed properties for your Vuex store.

To create a getter, open your src/store/index.js file in your text editor of choice. Once open, create a property in the getters object with a function as its value. The name of the property is how you will access the getter later.

Add the following highlighted code:

favorite-airports/src/store/index.js
import { createStore } from 'vuex'

export default createStore({
  state: {
    firstName: 'John',
    lastName: 'Doe'
  },
  ...
  getters: {
    fullName: function () {

    }
  }
})

In this case, you will use the function to combine the first and last names and store the resulting property as fullName. Inside the function, you will need to pass in the state object that is inside your Vuex store. From there, return a string with the first and last name interpolated:

favorite-airports/src/store/index.js
import { createStore } from 'vuex'

export default createStore({
  state: {
    firstName: 'John',
    lastName: 'Doe'
  },
  ...
  getters: {
    fullName: function (state) {
      return `${state.firstName} ${state.lastName}`
    }
  }
})

You are using template literals here to put the firstName and lastName into one string.

Save this file, then move back to App.vue. In this file, remove the first and last values and replace them with the getter:

favorite-airports/src/App.vue
<template>
  <div class="wrapper">
    <p>{{ $store.getters.fullName }}</p>
    <div v-for="airport in airports" :key="airport.abbreviation">
      <airport-card :airport="airport" />
    </div>
  </div>
</template>
...

Once you make this change and save the file, your browser will hot reload. You will see the first and last name in your browser as before, but now you are leveraging the getter. If you change one of the names in your Vuex store, the getter will be updated automatically.

Moving on from getters, there are actions. As mentioned in the last step, for the sake of this tutorial, you will always use an action rather than mutating the data directly.

In this project, you will add an airport’s data to a “favorites” list when a user clicks on the card. You are going to first get the action and mutation created, then later assign it to a click event using the v-on directive.

To create an action, open the src/store/index.js file in your text editor. In the actions section of the store, create a function. Like the getter, the function name will be how you reference the action later. Name this function addToFavorites:

favorite-airports/src/store/index.js
import { createStore } from 'vuex'

export default createStore({
  state: {
    firstName: 'John',
    lastName: 'Doe',
    favorites: [] // will store favorites here
  },
  mutations: {

  },
  actions: {
    addToFavorites() {

    }
  },
  getters: {
    fullName: function (state) {
      return `${state.firstName} ${state.lastName}`
    }
}
})

An action accepts two arguments: the context, or the Vue app itself, and the payload, or the data that you want to add to the store. The context has a commit method associated with it that you will use to call a mutation you will make later:

favorite-airports/src/store/index.js
import { createStore } from 'vuex'

export default createStore({
  state: {
    firstName: 'John',
    lastName: 'Doe',
    favorites: []
  },
  mutations: {

  },
  actions: {
    addToFavorites(context, payload) {
      context.commit('UPDATE_FAVORITES', payload)
    }
  },
  getters: {
    fullName: function (state) {
      return `${state.firstName} ${state.lastName}`
    }
 }
})

The commit method also accepts two arguments: the name of the mutation to call and the payload or the data that the mutation will replace the state with.

In this code, you are naming the mutation UPDATE_FAVORITES. Mutation names should be agnostic and not named after a specific action. For example, a mutation like ADD_FAVORITE and REMOVE_FAVORITE implies a logic, like removing or adding a piece of data. This is not ideal, since mutations should have one job and one job only: update the state. To differentiate between adding and removing data, you could have two different actions that remove or add a favorite airport from the array, which then execute a single mutation called UPDATE_FAVORITES that updates the array with whatever was passed in. Minimizing the amount of mutations you have in your store will help make your Vuex store easier to manage as it grows larger in complexity and size.

Next, add some logic to this action. When you add an airport as a “favorite”, you will add that payload (the airport data) to the existing array. To do that, you can use the push method in JavaScript:

favorite-airports/src/store/index.js
import { createStore } from 'vuex'

export default createStore({
  state: {
    firstName: 'John',
    lastName: 'Doe',
    favorites: []
  },
  mutations: {

  },
  actions: {
    addToFavorites(context, payload) {
      const favorites = context.state.favorites
      favorites.push(payload)
      context.commit('UPDATE_FAVORITES', favorites)
    }
  },
  getters: {
    fullName: function (state) {
      return `${state.firstName} ${state.lastName}`
    }
 }
})

At this point, your action is set up to to add the payload to your favorites array then call a mutation with the mutated array as the new data. Next, you will define the UPDATE_FAVORITES mutation. Add the following code to set the favorites array as a new array:

favorite-airports/src/store/index.js
import { createStore } from 'vuex'

export default createStore({
  state: {
      firstName: 'John',
    lastName: 'Doe',
    favorites: []
    },
  mutations: {
    UPDATE_FAVORITES(state, payload) {
      state.favorites = payload
    }
  },
  actions: {
    addToFavorites(context, payload) {
      const favorites = context.state.favorites
      favorites.push(payload)
      context.commit('UPDATE_FAVORITES', favorites)
    }
  },
  getters: {
    fullName: function (state) {
      return `${state.firstName} ${state.lastName}`
    }
 }
})

Now that you have your action and your mutation, you can save this file.

To execute this action, you can call a dispatch event when the user clicks on a card. You will do this with the v-on directive.

Open the App.vue file in your text editor. On the <airport-card /> component, add the v-on directive shorthand syntax (@) with the event being click:

favorite-airports/src/App.vue
<template>
  <div class="wrapper">
    <p>{{ $store.getters.fullName }}</p>
    <div v-for="airport in airports" :key="airport.abbreviation">
      <airport-card :airport="airport" @click="$store.dispatch('addToFavorites', airport)" />
    </div>
    <h2 v-if="$store.state.favorites.length">Favorites</h2>
    <div v-for="airport in $store.state.favorites" :key="airport.abbreviation">
      <airport-card :airport="airport"  />
    </div>
  </div>
</template>
...

The dispatch function accepts two arguments: the action name and the payload data that you are sending to the action.

Save this file and open it in your browser. Now, when you click on an airport card, the action will call the mutation that updates the state and adds the airport to the favorites property.

A favorite airport added to the favorite airports section after a Vuex mutation was executed.

In this step, you expanded on the Vuex store that you created earlier. You created an action that copies an array and pushes a new item to that array. That action called a mutation that in turn updated the state. In addition to that, you learned about getters and how they can be leveraged to create new properties by combining or modifying read-only values in the Vuex store.

In the final step, you will implement Vuex modules. Modules are a great way to break up your Vuex store into smaller Vuex stores. This is useful as your Vuex store growers larger in size and complexity.

Step 4 — Composing Vuex Modules

Modules are smaller Vuex stores that are combined into a single Vuex store. This is similar to how multiple Vue.js components are imported into a single .vue file, such as App.vue. In this step, you are going to separate this Vuex store into two separate modules. One module will be for a user state, and the other will be specific to airport state, actions, and mutations.

In your terminal, cd into the store directory and use the touch command to create two separate files.

  • touch src/store/user.module.js
  • touch src/store/airports.module.js

Inside of the user.module.js file, create an object that will be exported by default by adding the following code:

favorite-airports/src/store/user.module.js
export default {
  namespaced: true
}

You are also adding the property namespaced with its value as true. The namespace property will make it so you can reference the module name while accessing a property with dot notation later.

Inside of this object, you will add the state and getter information that is associated with a user:

favorite-airports/src/store/user.module.js
export default {
  namespaced: true,
  state: {
    firstName: 'John',
    lastName: 'Doe'
  },
  getters: {
    fullName: function (state) {
      return `${state.firstName} ${state.lastName}`
    }
  }
}

The user module contains everything that you need for user information. Save and exit from the file.

Go ahead and do the same thing in the airports.module.js file. Open the airports.module.js file in your text editor and add the following:

favorite-airports/src/store/airports.module.js
export default {
  state: {
    favorites: []
  },
  mutations: {
    UPDATE_FAVORITES(state, payload) {
      state.favorites = payload
    }
  },
  actions: {
    addToFavorites(context, payload) {
      const favorites = context.state.favorites
      favorites.push(payload)
      context.commit('UPDATE_FAVORITES', favorites)
    }
  },
}

Now that you have put the airport-related mutations, actions, and state, you can save your airports.module.js.

Next, import these two files into the main store/index.js file. In your text editor, open the store/index.js file and remove the state, mutations, actions, and getters properties. Your file will resemble the following snippet:

favorite-airports/src/store/index.js
import { createStore } from 'vuex'

export default createStore({

})

To register modules, you will need to import them into this index.js file with the following highlighted code:

favorite-airports/src/store/index.js
import { createStore } from 'vuex'
import UserModule from './user.module.js'
import AirportsModule from './airports.module.js'

export default createStore({

})

From here, you will need to have a property called modules with an object as its value. The property names inside of this object will be the names of the Vuex modules. The value of the module name is the imported module itself:

favorite-airports/src/store/index.js
import { createStore } from 'vuex'
import UserModule from './user.module.js'
import AirportsModule from './airports.module.js'

export default createStore({
modules: {
  user: UserModule,
  airports: AirportsModule
}
})

Once you save this file, your modules have now been registered and combined into your single Vuex store. Save store/index.js, then open the App.vue file and update it to reference the newly created modules:

favorite-airports/src/App.vue
<template>
  <div class="wrapper">
    <p>{{ $store.getters['user/fullName'] }}</p>
    <div v-for="airport in airports" :key="airport.abbreviation">
      <airport-card :airport="airport" @click="$store.dispatch('addToFavorites', airport)" />
    </div>
    <h2 v-if="$store.state.airports.favorites.length">Favorites</h2>
    <div v-for="airport in $store.state.airports.favorites" :key="airport.abbreviation">
      <airport-card :airport="airport"  />
    </div>
  </div>
</template>
...

Now you have a modular version of your Vuex setup.

In this step, you segmented your existing Vuex store into smaller chunks called modules. These modules are a great way to group related store properties into a smaller Vuex store. You also updated your App.vue to reference the state and dispatch events in each module.

Conclusion

At a high level, state management is all about updating data. In this setup, the data in the state is global throughout your entire application and acts as a single source of truth, which can only be updated with explicit functions in the form of actions and mutations. In this tutorial, you ran through examples of state, mutations, actions, and getters and saw how each of these properties have their own purpose in the update cycle.

To learn more about Vuex, actions, mutations, and modules, review the official Vuex documentation written by the Vue.js Core Team. For more tutorials on Vue, check out the How To Develop Websites with Vue.js series page.

Source: DigitalOcean