Sign Up
Hero

State Management in Vue 2 Applications

Learn how to implement State Management in Vue 2 using Vuex

TL;DR: In today's world of front-end applications and their component-based architectures, components will most often need to share data amongst themselves to be displayed or used to achieve certain functionality. Keeping data synced and up-to-date amongst components in a modern front-end application can quickly become a tedious and frustrating task if not tackled using a battle-tested strategy. This has led to the development of quite a several state management libraries with certain frameworks having a de-facto state management library that they naturally work well with (e.g. Redux for Reactjs). In this tutorial, you will learn and demonstrate how to perform state management in Vue 2 applications using Vue's standard state management library, Vuex.

What We Are Going to Build

In this tutorial, you will be implementing state management in a shopping cart built with Vue 2. You will begin with an already built e-commerce application that sells different types of wares; however, this application is yet to be complete as its shopping features for adding products to the cart, and managing cart items are not implemented yet. Using the Vuex state management library, you will be adding the following features to the application:

  • Loading the products from a central state store
  • Adding items to the shopping cart
  • Displaying items added to the shopping cart
  • Calculate the total price of items in the cart
  • Increase and decrease the quantity of the items in the cart
  • Remove items from the cart

Prerequisites

To follow along with this exercise, there is a couple of setup/knowledge required:

  • Node.js installed on your system. You can visit the website and install a suitable version for your operating system here.
  • Vue CLI installed on your system. You can install this by running the command: npm install -g @vue/cli
  • Basic knowledge of Vue and using Vue components.

With all these in place, you can proceed with the exercise.

Cloning the Demo E-commerce Project

To begin, you will clone the starting point for the project and run it. Run the following command to clone a copy of the e-commerce project unto your machine:

git clone --single-branch --branch base-project https://github.com/coderonfleek/vue2-store

Once you have the project on your system, go into the root of the project (cd vue2-store) and run the following command to install the dependencies:

npm install

With all dependencies installed, you can then run the project using the following command:

npm run serve

This command will boot up a local development server at http://localhost:8080, navigate to this address on your browser, and you will see the page displayed below:

At the moment, when you click Add to Cart on any of the products, the button does nothing. The source code for the homepage is located in the file src/views/Home.vue. The products are currently loaded from a products array on the page using a Product component to display the products:

<template>
  <div class="home container">
    <div class="row">
      <div class="col-md-9 pt-5">
        <div
          class="row row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-3 row-cols-xl-3"
        >
          <Product
            v-for="product in products"
            :product="product"
            :key="product.id"
          />
        </div>
      </div>
      <div class="col-md-3 pt-5">
        <Cart />
      </div>
    </div>
  </div>
</template>

On the right column, a Cart component is used to display items that have been added to the cart and their respective quantities, which is also hard-coded into the component at the moment. The Cart component also consists of a Checkout button which will navigate to the full shopping cart details page shown below when clicked:

The cart details page source code can be found in src/views/ShoppingCart.vue. It uses a CartItem component to display any item added to the cart with buttons to increase or decrease the quantity of the item in the cart or remove it completely. The Cart component is also displayed on the right column, just as it was on the homepage. At the moment, these buttons do nothing when clicked.

<template>
  <div class="home container">
    <div class="row">
      <div class="col-md-8 pt-5">
        <CartItem
          v-for="product in cart"
          :product="product"
          :key="product.id"
        />
      </div>
      <div class="col-md-4 pt-5">
        <Cart />
      </div>
    </div>
  </div>
</template>

The cart items displayed on this page are loaded from a cart array and currently hard-coded on the page. What's more, clicking any of the buttons does nothing at the moment. All components used in the application are contained in the src/components folder.

Creating the State Management Store

To begin implementing the features listed in the above section, you will need to set up a central state management store in the application. Install the vuex library by running the following command at the root of the project directory

npm install vuex --save

Once installed, create a store.js inside the src folder and add the following code:

import Vue from "vue";

import Vuex from "vuex";

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    products: [
      {
        id: 1,
        name: "Chelsea Shoes",
        price: 200,
        shortdesc: "Best Drip in the Market",
        url: "images/chelsea-shoes.png"
      },
      {
        id: 2,
        name: "Kimono",
        price: 50,
        shortdesc: "Classy, Stylish, Dope",
        url: "images/kimono.png"
      },
      {
        id: 3,
        name: "Rolex",
        price: 2500,
        shortdesc: "Elegance built in",
        url: "images/rolex.png"
      },
      {
        id: 4,
        name: "Baelerry Wallet",
        price: 80,
        shortdesc: "Sleek, Trendy, Clean",
        url: "images/wallet.png"
      },
      {
        id: 5,
        name: "Lady Handbags",
        price: 230,
        shortdesc: "Fabulous, Exotic, Classy",
        url: "images/handbag.png"
      },
      {
        id: 6,
        name: "Casual Shirts",
        price: 30,
        shortdesc: "Neat, Sleek, Smart",
        url: "images/shirt.png"
      }
    ],
    cart: []
  },
  mutations: {
    addCartItem(state, item) {
      item.quantity = 1;
      state.cart.push(item);
    },
    updateCartItem(state, updatedItem) {
      state.cart = state.cart.map((cartItem) => {
        if (cartItem.id == updatedItem.id) {
          return updatedItem;
        }

        return cartItem;
      });
    },
    removeCartItem(state, item) {
      state.cart = state.cart.filter((cartItem) => {
        return cartItem.id != item.id;
      });
    }
  }
});

The state property in the above store consists of two properties; the products property holds all products contained in the e-commerce application. In a production scenario, you would want to load products from a remote API using a Vuex action and commit it to state using a mutation. The other state property is cart which is an array that holds the items a user adds to their cart; this is empty by default.

Then there are three (3) Vuex mutations that manage the state properties. The addCartItem, updateCartItem, and removeCartItem mutations add a new item to the cart, update an item in the cart and remove an item from the cart array, respectively.

These properties are all that is needed to implement the features listed above. To complete the state management store setup, replace the code in src/main.js with the following:

import Vue from "vue";
import App from "./App.vue";
import router from "./router";

Vue.config.productionTip = false;
import store from "./store";

new Vue({
  router,
  store,
  render: (h) => h(App)
}).$mount("#app");

The update to the code in this file imports the store you just created and registers it on the root Vue instance for the application.

Loading Products to the Home Page from the Store

The first task is to ensure that the products are loaded from the store instead of being hard-coded on the Home.vue homepage. Locate src/views/Home.vue and replace its content with the following code:

<template>
  <div class="home container">
    <div class="row">
      <div class="col-md-9 pt-5">
        <div
          class="row row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-3 row-cols-xl-3"
        >
          <Product
            v-for="product in products"
            :product="product"
            :key="product.id"
          />
        </div>
      </div>
      <div class="col-md-3 pt-5">
        <Cart />
      </div>
    </div>
  </div>
</template>

<script>
import Product from "../components/Product.vue";
import Cart from "../components/Cart.vue";

import { mapState } from "vuex";

export default {
  name: "Home",
  components: {
    Product,
    Cart
  },
  data() {
    return {};
  },
  computed: {
    ...mapState({
      products: (state) => state.products
    })
  }
};
</script>

In the update above, the products data variable has now been replaced with a computed property with the same name that references the store to load the products. Now refresh your homepage, and you will not see any change, but you know that your products are now being loaded from the store.

Adding Items to the Shopping Cart

The next task is to implement the functionality that allows users to click Add to Cart on a product on the homepage and see it added to the cart widget on the right column. As mentioned earlier, products displayed on the page are managed using a Product component, locate this component at src/components/Product.vue and replace its content with the following code:

<template>
    <div class="col mb-4">
        <div class="card">
            <img :src="product.url" class="card-img-top" alt="...">
            <div class="card-body">
            <h5 class="card-title">{{product.name}}</h5>
            <p class="card-text">
                ${{product.price}}
                <br/>
                <small>
                {{product.shortdesc}}
                </small>
            </p>
            <button @click="addToCart()" class="btn btn-primary btn-block" :disabled="itemAlreadyInCart">{{itemAlreadyInCart? "Added" : "Add to Cart"}}</button>
            </div>
        </div>
    </div>
</template>

<script>
import {mapState} from "vuex";

export default {
    name : "Product",
    props : ['product'],
    computed : {
        ...mapState({
            cart : state => state.cart
        }),
        itemAlreadyInCart(){
            let inCart = false;

            this.cart.forEach(item => {
                if(item.id == this.product.id){
                    inCart = true;
                }
            });

            return inCart;
        }
    },
    methods : {
        addToCart(){

            if(!this.itemAlreadyInCart){
                this.$store.commit("addCartItem", this.product);
            }else{
                alert("Item already added to Cart");
            }
        }
    }
}
</script>

The updates to this file add two computed properties to the component. The cart property references the cart property in the store while the itemAlreadyInCart checks if the product using this component has been added to the store or not.

In the methods object, the addToCart function adds an item to the cart when the Add to Cart button is clicked by calling the addCartItem mutation in the store and passing in the product as a payload. This function first checks if the product is already in the cart; if not, the product is added; else, an alert is displayed indicating that the product has already been added.

The itemAlreadyInCart property is also used in the template to disable the Add to Cart button if the product has already been added.

Next, the Cart component needs to display the products that have been added to the cart and also the total price. Locate the src/components/Cart.vue file and replace its content with the following code:

<template>
    <div class="card">
        <div class="card-body">
            <h5 class="card-title">Your Cart</h5>
            <p v-if="cart.length == 0">
                Your Cart is Empty
            </p>
        </div>
        <ul class="list-group list-group-flush">
            <li v-for="item in cart" :key="item.id" class="list-group-item d-flex justify-content-between align-items-center">
                {{item.name}}
                <span class="badge badge-primary badge-pill">{{item.quantity}}</span>
            </li>
            <li class="list-group-item d-flex justify-content-between align-items-center">
                Price <b>${{totalPrice}}</b>
            </li>
        </ul>

        <div class="card-body">
            <router-link to="/shop" class="btn btn-primary btn-block">Checkout</router-link>
        </div>
    </div>
</template>

<script>
import {mapState} from "vuex";

export default {
    computed : {
        ...mapState({
            cart : state => state.cart
        }),
        totalPrice(){
            return this.cart.reduce((total, next) => {
                return total + (next.quantity * next.price)
            }, 0)
        }
    }
}
</script>

The update to this file first replaces the hard-coded cart items with reference to that cart state in the store. A totalPrice computed property is also used to evaluate the total cost of all currently added to the shopping cart. In the template, the cart is used to display a message to the user to add items to the shopping cart when the cart is empty.

Return to the browser and reload the homepage if it hasn't been reloaded. You will see the view below; notice the new look of the cart widget on the right column:

Now click on Add to Cart to add at least 2 items to the cart and observe the cart widget update based on your selections like below:

Changing / Removing Items from the Cart

Notice how the default quantity of each product added to the cart is set to one (1)? This is because every count has to start from 1. In a proper shopping cart application, users should be allowed to increase the quantity of each selected item.

The next task is to add features for increasing and decreasing the number of cart items. What's more, you would also add the ability to completely remove an item from the cart.

From the shopping cart details page shown earlier, each cart item displayed with buttons to manage its quantity uses the CartItem component. Locate this component at src/components/CartItem.vue and replace the code in it with the following:

<template>
    <div class="row cart-item-row">
        <div class="col-md-6">
            <Product :product="product" />
        </div>
        <div class="col-md-4">
            <div class="row">
                <div class="col-md-5">
                    <button @click="changeQuantity()" class="btn btn-primary btn-block">+</button>
                </div>
                <div class="col-md-2 text-center">{{itemQuantity}}</div>
                <div class="col-md-5">
                    <button @click="changeQuantity('decrease')" class="btn btn-warning btn-block">-</button>
                </div>
            </div>
            <div class="row cart-remove-button">
                <div class="col-md-12">
                    <button @click="removeItem()" class="btn btn-danger btn-block">Remove Item</button>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
import Product from "./Product.vue";
import { mapState } from "vuex";
export default {
    name : "CartItem",
    props : ['product'],
    components : {
        Product
    },
    computed : {
        ...mapState({
            cart : state => state.cart
        }),
        itemQuantity(){
             let get_product = this.cart.filter((item) => item.id == this.product.id);

             return get_product[0].quantity;
        }
    },
    methods: {
        changeQuantity(action = 'add'){

            if(action == 'add'){
                this.product.quantity = this.product.quantity + 1;

                this.$store.commit("updateCartItem", this.product);
            }else{

                if(this.product.quantity > 1){
                    this.product.quantity = this.product.quantity - 1;
                    this.$store.commit("updateCartItem", this.product);
                }else{
                    //Remove the item
                    this.$store.commit("removeCartItem", this.product)
                }
            }
        },
        removeItem(){
            this.$store.commit("removeCartItem", this.product)
        }
    }
}
</script>
<style scoped>
.cart-item-row{
    border-bottom: 1px solid #ccc;
    margin-top: 20px;
}
.cart-remove-button{
    margin-top: 10px;
}
</style>

This update adds two computed properties, cart, which is a reference to the cart state property in the store, and itemQuantity, which gets the current quantity of the item in the shopping cart.

Two methods are also added which do the following:

  • changeQuantity: takes in an action argument that is either set to add or decrease to determine whether to increase or decrease the item quantity by 1 (one). If the current quantity is 1 and the function is asked to decrease the item, the item would be removed completely.
  • removeItem: completely removes an item from the shopping cart.

Next, locate the shopping cart details page at src/views/ShoppingCart.vue and replace its content with the following code:

<template>
  <div class="home container">
    <div class="row">
      <div class="col-md-8 pt-5">

       <CartItem v-for="product in cart" :product="product" :key="product.id" />

      </div>
      <div class="col-md-4 pt-5">
        <Cart />
      </div>

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

<script>
import CartItem from "../components/CartItem.vue";
import Cart from "../components/Cart.vue";
import {mapState} from "vuex";

export default {
  name: 'ShoppingCart',
  components: {
    CartItem,
    Cart
  },
  data(){
    return {

    }
  },
  computed : {
    ...mapState({
        cart : state => state.cart
    }),
  }
}
</script>

This update replaces the hard-coded cart items with reference to the cart property in the store.

Running the Application

With these changes, your app should be reloaded once again. Add a few items to the shopping cart if the reload has reset the cart back to being empty, then click Checkout to go to the shopping cart details page. On this page, increment and decrement some of the items in the cart and try removing one of them by clicking Remove. You will see the cart widget update accordingly on the right column like below:

Smooth, isn't it.

Conclusion

One of the major considerations when setting up state management in your applications is to properly decide on which state properties are global (reside in the store) and which state properties are to be localized to the respective components that make use of them. For example, properties like the total price of items in the store or whether an item has been added to the store or not are contained within the components that require them mostly because these values can easily be derived from the central state properties that have been kept in the central store, i.e., the products and the collection of items in the cart.

In this tutorial, you have been able to implement state management in a Vue 2 application using a shopping cart application as a proof of concept. If any part of your demonstration is not working as expected, I advise that you go through the article once again to see if there is any step you may have missed or feel free to reach to me in the comments section.

Happing Coding :)