Pinia - Crash Course for Beginners
Tags: javascript, vue, tutorial, webdev
Published: 2023-03-24 08:38:00

## Introduction

Hello everyone, in this crash course we will be talking about Pinia - the state management library for Vue. We will also be building a simple shopping cart project to showcase how Pinia stores work.

You can follow along here:

{% embed https://www.youtube.com/watch?v=iuBLAdUWPtQ %}

The code for the project can be found on my GitHub: [https://github.com/alexander-gekov/pinia-cart-tutorial](https://github.com/alexander-gekov/pinia-cart-tutorial)

## Content

- [What is Pinia? (store, state, getters, actions)](#what-is-pinia?)
- [Creating a Shopping Cart Project with Pinia](#shopping-cart-app)
- [Setting up a Vue.js app with Vite](#setting-up-a-vue.js app-with-vite)
- [Installing and Registering Pinia](#installing-and-registering-pinia)
- [Creating a Pinia Store](#creating-the-product-pinia-store)
- [Using store in component and Pinia DevTools](#using-the-pinia-store-in-a-component)
- [Creating the Cart Store](#creating-the-cart-store)
- [Adding functionality for Cart](#adding-functionality-for-cart)
- [Conclusion](#conclusion)
- [Useful resources](#useful-resources)

## What is Pinia?

> Pinia is a state management library for Vue.js.

It is the successor of Vuex the original state management system for Vue. However, nowadays Pinia is the recommended way to manage state as said by the Vue.js Core Team. 

The problem that state management solves is the problem of keeping shared state across your Vue components. Without it a lot of the state would have to be passed around using endless amount of props and emits. 

![Prop drilling](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jzdkr1dncyzc27ay1hwt.jpeg)

Let’s say we have a user who is authenticated, we would like to share that state across our components. We can also later store it in a cookie or local storage so that it persists even after page loads.

![User state](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6lntca2inc8zsgrltgn8.png)

---

Pinia uses “stores” in order to manage state. A **Store** is compromised of:

- state (The data that we want to share)
- getters (A way for us to get the data from the state, read only)
- actions (Methods that we can use to modify/mutate the data in the state)
- ~~mutations~~ - One of the differences between Pinia and Vuex is that Pinia does not have explicit mutations defined in the store. They were removed from the store definition due to being too verbose. Read till the end to find out how to monitor mutations in your app.

Another differences between Pinia and Vuex is that Pinia is modular. What that means is that Pinia encourages users to have different stores, each corresponding to a different logic in our app. This is a great approach because it follows the separation of concern principle. On the other hand Vuex had developers manage a single bulky store for the whole application. You can see where I am going with this…

Pinia also has great Typescript support and autocompletion. This helps with the overall development experience, a metric that is becoming ever more important in modern frontend tools. 

![ts](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i1hpdintynepln0bz3j2.png)

Another reason to try out Pinia, last one I promise 😃, is that it is incredibly lightweight with a total bundle size of just 1KB.

## Shopping Cart App

What we are going to build today to showcase how Pinia works is a simple ecommerce-like website. We will have some products that the user can add to their cart. The cart in the top right corner will need to keep track of the items that the user has added. It will also have to keep track of the quantity of each product as well as calculate the total count and total price.

The design was made with tailwindcss, however, this article will not be focusing on the styling part. If you want you can go to the GitHub repository and clone it to start with a simple UI template I prepared with hardcoded values. 

![project](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4kyre43sn386awlijeh0.png)

## Setting up a Vue.js app with Vite (using vue-ts template)

Let’s start by creating our Vue app. You can use the Vue CLI or Vite for this. I will use Vite as well as the “vue-ts” template provided by vite.

```tsx
npm create vite@latest pinia-cart-tutorial -- --template vue-ts
```

Once created, cd into the just created project folder and open it in VS Code. In VS Code I will open the terminal and run `npm install`.

Now, we can remove the boilerplate code - so remove the Hello World and any unnecessary styling. As I mentioned I won’t go over creating the components and styles, but feel free to look at the GitHub link.

## Installing and registering Pinia

Let’s install Pinia by running:

```tsx
npm install pinia
```

Once done, we need to go to our `main.ts` file and register it like this:

```tsx
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import { createPinia } from 'pinia'

createApp(App).use(createPinia()).mount('#app')
```

Now, Pinia should be successfully installed.

## Creating the Product Pinia Store

Currently we have an array in our App.vue that we are passing to our components through props. While this is by any means, a valid and totally ok way to pass the state. Imagine if the app was more complex and contained a lot more nested components. We should create a store to keep that state of products.

In our `src` folder, let’s create a folder called `stores`. In there create a file `ProductStore.ts`:

```tsx
import { defineStore } from "pinia";
import { computed, ref, Ref } from "vue";
import { Product } from "../types/Product";

export const useProductStore = defineStore('products', () => {
    const products: Ref<Product[]> = ref([
        {
          name: 'Bananas',
          price: 5,
          image: 'https://img.freepik.com/free-vector/vector-ripe-yellow-banana-bunch-isolated-white-background_1284-45456.jpg'
        },
        {
          name: 'Strawberries',
          price: 10,
          image: 'https://img.freepik.com/free-photo/strawberry-berry-levitating-white-background_485709-57.jpg?w=2000'
        },
        {
          name: 'Apples',
          price: 15,
          image: 'https://img.freepik.com/premium-photo/red-apples-isolated-white-background-ripe-fresh-apples-clipping-path-apple-with-leaf_299651-595.jpg'
        }
      ]); // ref = state

    const totalPrice = computed(
        () => products.value
        .map(p => p.price)
        .reduce((a, b) => a + b, 0)
    ); // computed = getter

    const addProduct = (product: Product) => {
        products.value.push(product);
    } // method = action

    return {
        products,
        totalPrice,
        addProduct
    }
})
```

1. We start by importing the `defineStore` method from `pinia`. We then use it to export a const called `useProductStore`. It is a common convention to prefix stores and composables with the “use” word.
2. `defineStore` accepts two arguments, the first being the name of the store, and the second depending on how we write our store (Options API vs Composition API) will either be the object containing the state, getters and actions or a callback that will return an object with the state, getters, actions.
3. In our store, we are using the Composition API so: `ref` and `reactive` become the state, `computed` variables become getters and methods become actions.

Here is a simple image showcasing Options API store vs Composition API Store:

![options vs comp api](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xxbh48rzcctyxvqr5pgg.png)

## Using the Pinia store in a component

Back in `App.vue` we can import it like this:

```tsx
<script>
import { useProductStore } from './stores/ProductStore';

const productStore = useProductStore();
</script>

<template>
..
</template>
```

As you can see we import it and then just initialize it. In order to verify that it’s working, we can open our app in Chrome and open Vue Devtools. There should be a new tab for Pinia, where you will be able to see your stores and other relevant data.

![devtools](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/udj6ndw4j1zvu677nge9.png)

What’s more we if we go to the Timeline tab and then to Pinia, we can monitor different actions and mutations happening throughout our app:

![timeline](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e6qo71v83xyxbqmdl7iv.png)

## Creating the Cart Store

Going back to the `stores` folder, let’s create `CartStore.ts`:

```tsx
import { defineStore } from 'pinia'
import { computed, ref, Ref } from 'vue';
import { Product } from '../types/Product';

export const useCartStore = defineStore('cart', () => {
    const items: Ref<Product[]> = ref([]); // state

    const itemsCount = computed(() => items.value.length); // getter

    const groupedItems = computed(() => {
         return items.value.reduce((acc, item) => {
            if (!acc[item.name]) {
                acc[item.name] = [];
            }
            acc[item.name].push(item);
            return acc;
        }, {} as Record<string, Product[]>);
    }); // getter

    const addItem = (item: Product) => {
        items.value.push({...item});
    } // action

    const removeItem = (item: Product) => {
        const index = items.value.findIndex(i => i.name === item.name);
        items.value.splice(index, 1);
    } // action

    const $reset = () => {
        items.value = [];
    } // action $reset

    return { items, itemsCount, groupedItems, addItem, removeItem, $reset}
});
```

In this store we keep an array which will hold the products. We have getters for the total count of all products as well as grouping them by their name. This is used to later get the quantity of each product. We also have some actions for adding and removing a product as well as a reset method that will reset the state of the store.

## Adding functionality for Cart

Our `App.vue` should look like this:

```tsx
<script setup lang="ts">
import { ref } from 'vue';
import Card from './components/Card.vue';
import Cart from './components/Cart.vue';
import NavBar from './components/NavBar.vue';
import { useProductStore } from './stores/ProductStore';
import { useCartStore } from './stores/CartStore';

const showCart = ref(false);

const productStore = useProductStore();
const cartStore = useCartStore();

</script>

<template>
  <div class="relative max-w-6xl mx-auto">
    <!-- NavBar contains the toggle for the cart -->
    <NavBar :show-cart="showCart" @toggleCart="showCart = !showCart"/>
    <!-- Cart Dropdown -->
    <Cart v-if="showCart"/>
    <main class="flex flex-1">
      <!-- Product Card -->
      <Card v-for="product in productStore.products" :key="product.name" :product="product" @add-to-cart="cartStore.addItem"/>
    </main>
  </div>
</template>

<style scoped>
</style>
```

1. NavBar - the NavBar contains the shopping cart icon. The NavBar emits an event `toggleCart` and that is used to toggle the state of `showCart` which itself toggles the Cart dropdown.
2. Cart - which is the Cart Dropdown, where the added products will be displayed. More on that in a moment.
3. Product Card - these are our products, we use the `productStore` to loop over all the products and display them. The Card also emits an event called `addToCart.` We then use the `cartStore` to call the `addItem` action and add the product to the state.

This is how `NavBar.vue` shoud look like:

```tsx
<template>
    <nav class="border border-gray-300 rounded-xl rounded-t-none p-4 mb-10">
      <div class="container flex items-center justify-between">
        <h2 class="font-bold text-2xl w-1/3">Pinia Cart Tutorial</h2>
        <div class="w-1/3">
          <img class="w-12" src="/pinia.png" alt="">
        </div>
        <button @click="$emit('toggleCart')" class="relative hover:bg-gray-200 rounded-full p-2">
          <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
          <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 00-3 3h15.75m-12.75-3h11.218c1.121-2.3 2.1-4.684 2.924-7.138a60.114 60.114 0 00-16.536-1.84M7.5 14.25L5.106 5.272M6 20.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm12.75 0a.75.75 0 11-1.5 0 .75.75 0 011.5 0z" />
          </svg>
          <div class="bg-red-500 rounded-full px-2 absolute -top-2 -right-2">{{ itemsCount }}</div>
        </button>
      </div>
    </nav>
</template>

<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { useCartStore } from '../stores/CartStore';

defineProps({
    showCart: {
        type: Boolean,
        default: false
    },
})

const {itemsCount} = storeToRefs(useCartStore());
</script>
```

1. We emit an event “toggleCart”
2. We have a bubble notifying us how many products are in the cart
3. We import the `useCartStore`, however when we want only one or two variables, we can destructure our store using the `storeToRefs` helper. It will unsure that reactivity is preserved when destructuring our store.

Lastly, this is how `Cart.vue` looks like:

```tsx
<template>
    <div class="absolute top-20 right-0 w-1/3 p-4 border rounded-lg bg-white border-gray-300">
          <h1 class="text-xl font-bold">My Cart</h1>
          <ul>
            <li v-for="[name, items] in Object.entries(cartStore.groupedItems)" :key="name">
              <div class="flex items-center justify-between py-2">
                <span>{{name}}</span>
                <div class="flex items-center">
                  <button @click="cartStore.removeItem(items[0])" class="hover:bg-gray-200 rounded-full p-2">
                  -
                </button>
                <span class="mx-2">{{ items.length }}</span>
                <button @click="cartStore.addItem(items[0])" class="hover:bg-gray-200 rounded-full p-2">
                  +
                </button>
                </div>
                <span>${{items.map(i => i.price).reduce((totalItemPrice, price) => totalItemPrice + price, 0)}}</span>
              </div>
            </li>
          </ul>
          <hr class="my-2">
          <div class="flex items-center justify-between">
            <span class="font-bold">Total</span>
            <span class="font-bold">${{ cartStore.items.map(p => p.price).reduce((acc,curr) => acc + curr, 0) }}</span>
          </div>
        </div>  
</template>

<script setup lang="ts">
import { useCartStore } from '../stores/CartStore';

const cartStore = useCartStore();
</script>
```

1. We import the `useCartStore` and we use it to get access to `items`,`groupedItems`, `removeItem` and `addItem`.
2. We loop over our `groupedItems` and display the name, then on removing or adding we use the item at index 0, so the item that is first in the array.
3. We also calculate the total price per product by using the `.reduce` method to sum up the individual costs.
4. Lastly we also calculate the total price for all products, again using the `.reduce` method

![cart](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tgsieh259kanvttu5zcq.png)

## Conclusion

We are now done with our application. You can go on and try adding items to cart. Changing the quantity to 100, or changing it 0 and it disappearing from the cart. 

I hope you liked this crash course about Pinia and managed to use it in our example app. If you have any questions don’t hesitate to reach out. 

## Useful Resources

You can always refer to the official documentation, it is really clear and helpful.

- [Pinia Docs](https://pinia.vuejs.org/introduction.html)
- [GitHub Shopping Cart Template Starter](https://github.com/alexander-gekov/pinia-cart-tutorial/tree/template)
- [VueSchool’s courses](https://vueschool.io/)

💚  If you want to learn more about Vue and the Vue ecosystem make sure to follow me on my socials. I create Vue content every week and am slowly starting to gain traction so I’d really appreciate your help!

[Twitter](https://twitter.com/AlexanderGekov)

[LinkedIn](https://www.linkedin.com/in/aleksandar-gekov/)

[YouTube](https://www.youtube.com/channel/UCAARceUUB_X6xsgeRBRg16A)