Integrating Vuex

Now we have introduced the Product Details page, we need a way to access that Product's information so we can display it on the frontend. The problem is, passing the whole product via the router link won't work and is not a scaleable approach.

Instead, we need a central place to manage the data state and to give the Product Details component the ability to query the state based on the dynamic ID we pass in the URL.

Learning Objectives

Re-imagining our app using Vuex

Let's first visualise how the Vuex store will fit in to our project:

Module 1 - Vuex store

Key points:

Vuex installation and configuration

Step 1

In your index.html file, add the following script tag above main.js:

<script src=" https://unpkg.com/vuex@3.6.2/dist/vuex.js"></script>

Step 2

Add three new objects:

const products = {
  state: {
    products: [
      {
        productId: 1,
        name: 'Boston Fern',
        description:
          'Nephrolepis exaltata, known as the sword fern or Boston fern, is a species of fern in the family Lomariopsidaceae native to tropical regions throughout the world.',
        features: ['Moisture loving', 'Easy care', 'Dislikes direct sun'],
        stockLevel: 5,

        // add rest of product JSON here
      },
    ],
  },
  getters: {},
};
const cart = {
  state: {
    cart: [],
  },
  mutations: {},
  getters: {},
};
const business = {
  state: {
    brand: 'Plants Direct',
    address: '21 Sussex Gardens',
    city: 'London',
    postcode: 'SW1 01L',
  },
  getters: {},
};

Step 3

Create a new Vuex store and pass in the modules you created above:

const store = new Vuex.Store({
  modules: {
    products,
    cart,
    business,
  },
});

Step 4

Pass the store into your app component:

var app = new Vue({
  router,
  store,
  el: '#app',

  // other fields omitted

Step 5

Remove {{this.cart.length}} from the home page.

Step 6

Verify your site is now loading. It won't show any products or other information just yet. We'll fix this below.

Configuring components to use Vuex store

As you'll know, all of our components use data from the main app component, but now we want to change this so they get the data from our newly created store. Let's fix the issues one by one:

Fixing the footer brand and address

Step 1

In the business module, add the following getters:

// Business module

getters: {
  getFullAddress(state) {
    return `${state.address}, ${state.city}, ${state.postcode}`;
  },
  getBrand(state) {
    return state.brand;
  },
},

The state parameter here is somewhat abstract as we don't actually pass in the state when using the getter methods. In this instance, it refers to the current data state of the store.

Step 2

In your app component, modify the computed methods as follows:

// app component

computed: {
  footerCopyrightNotice() {
    return '2021 ' + store.getters.getBrand;
  },
  footerAddress() {
    return 'Our address: ' + store.getters.getFullAddress;
  },
},

This should fix the footer output.

How do getters work?

In order to access our store, we now need to use getter methods that we create in the store modules. These methods can only read from the store, and can't modify or update it. In this case, getBrand and getFullAddress belong to the business module and return the brand name and the address.

We'll be using this getter pattern a good deal to fix the other data issues, so take a moment to review your changes to understand how it works.

Fixing the products display

Currently our products aren't displaying. This is because the Home component needs to grab them from the store and pass them down to the Product component to render them.

Step 1

Head to your index.html file and remove everything from router view:

<!-- index.html -- >

<router-view />

Step 2

Next up, add the getNProducts method to the Product store's getters object. This will allow us to get n number of properties for display on the home page:

// Product store

getters: {
  getNProducts: (state) => (n) => {
    return state.products.filter((element, index) => index < n);
  },
},

Step 3

Add a method to the Home component to get the products and use this in the template to loop through each product. Also, while you're there, remove the add-to-cart event as we'll no longer need it.

// Home component

methods: {
  getProducts(n) {
    return this.$store.getters.getNProducts(n);
  },
},
<!-- Home component -->

<product
  v-for="product in getProducts(3)"
  v-bind:key="product.productId"
  v-bind:product="product"
></product>

Step 4

Don't forget to remove the event from the router link in the Product component:

// Product component

<router-link
  v-bind:to="{ name: 'productDetails', params: {productId: product.productId} }"
  v-on:add-to-cart="addToCart" // remove this line
  class="anchor--button"
></router-link>

Step 5

Remove the updateCart method for now:

// Product component

// remove this method
updateCart(productId) {
  this.$emit('update-cart', productId);
},

This should now load the products onto the home page.

Fixing the Add to cart button

Currently the text changes on the Product's 'Add to cart' button, but we're not committing anything to the store and changing the cart.

Step 1

Cut the updateCart method from your app component and move it to the Cart modules's mutations object. This is the same solution you wrote for the coding challenge that adds/removes a Product ID from the cart.

It could look something like:

// Cart module

mutations: {
  updateCart(state, productId) {
    if (!state.cart.includes(productId)) {
      return state.cart.push(productId);
    }
    state.cart.splice(state.cart.indexOf(productId), 1);
  },
},

Be sure to remove the empty methods object from your app component.

Step 2

In your Product component, replace the addToCart method with the following version:

// Product component

addToCart(product) {
  product.addedToCart = !product.addedToCart;
  this.$store.commit('updateCart', product.productId);
},

Here we perform the NOT trick on the product then we use commit to commit the change to the store by passing the event name and the new value.

Step 3

Replace the cart length code in the header with:

<!-- index.html -->

<div class="cart">Cart ({{$store.getters.getCartLength}})</div>

The $store object is an object Vue exposes so we can query getters from a page such as the index page.

This should be enough to get your cart up and running again!

Fixing the Property detail content

The penultimate fix to get Vuex fully up and running is to get the Product Details to show up on the Product page. To do this, we'll use the dynamic ID we are passing via the router link to find the product by ID in the store. We'll then add this product to the local state via a lifecycle method so we can output the details. Let's go through it bit by bit:

Step 1

Firstly, add the method to find a product by ID to the Product module's getter methods object:

// Product module

getters: {
  getNProducts: (state) => (n) => {
    return state.products.filter((element, index) => index < n);
  },
  getProduct: (state) => (productId) => {
    return state.products.find(
      (product) => product.productId === parseInt(productId)
    );
  },
},

This is quirky construct like the getNProducts function that is a function that returns a function that returns the results of the find method. We need this in order for the state and the productId to be passed in.

Step 2

Next, we need to use the mounted lifecycle method to call the method we created above. We then assign the results to the local data state. We use this.$route.params.productId, which is another handy object Vue exposes that allows us to get the Product ID that we specified in the router link in Part 6.

// Main.js

const ProductDetails = Vue.component('Product-Details', {
  template: `
    // Template omitted
  `,
  data() {
    return {
      product: {},
    };
  },
  mounted() {
    this.product = this.$store.getters.getProduct(this.$route.params.productId);
  },
});

A lifecycle method is a group of events that fire at predictable times during the creation of a Vue component. In our case, the mounted method can be used to request data once the component has been mounted (added) to the DOM.

Step 3

The last step is to add in some methods to add the product to cart and to fetch the second image in the images array:

// Product Details component

methods: {
  getImage(product) {
    return product.images[1].imageSrc;
  },
  addToCart(product) {
    product.addedToCart = !product.addedToCart;
    this.$store.commit('updateCart', product.productId);
  },
},

You should now see the Product Details and be able to add/remove the product to/from the cart! In addition, the state should hold when you return to the home page. Add the product to the cart, return home, and you should still see the item in the cart. You should also see that the product's 'Add to cart' button has changed to 'Remove from cart'.

Assignment - Coding challenge

You may notice that there's an error in the console on the Property Details page:

vue:6 TypeError: Cannot read property '1' of undefined
    at a.getImage (main.js:234)
  1. Why is this error being thrown yet the image still loads?

  2. How can you fix it?