Skip to main content
Vue.js for Beginners to Advanced
CHAPTER 16 Beginner

State Management with Pinia

Updated: May 18, 2026
5 min read

# CHAPTER 16

State Management with Pinia

1. Chapter Introduction

When multiple components across an application need to share the same data, "prop drilling" becomes painful. Pinia solves this with a global reactive store — data declared once, accessible anywhere. Pinia is Vue 3's official state manager, replacing Vuex with a much simpler API.

2. Learning Objectives

  • Understand why global state management is needed.
  • Install and set up Pinia.
  • Create stores with state, getters, and actions.
  • Use stores in components.
  • Build a shopping cart with Pinia.

3. Why Pinia?

text
1234567891011121314151617
Without State Management (Prop Drilling):

App
├── Navbar (needs: cartCount)
│   └── CartIcon (needs: cartCount)
├── ProductList
│   └── ProductCard (emits: addToCart)
│       └── AddButton (emits: addToCart)
└── CartSidebar (needs: cartItems, total)

→ cartItems must be passed as props through EVERY level
→ Events must be emitted UP through EVERY level

With Pinia:
Store (cartItems, cartCount, total)
  ↓ Any component can directly access/update
Navbar, ProductCard, CartSidebar — all read from same store

4. Installation

bash
1
npm install pinia
main.js
1234567
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)
app.use(createPinia())
app.mount('#app')

5. Creating a Store

javascript
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
// src/stores/cart.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

// Composition API style (recommended — same as Composition API in components)
export const useCartStore = defineStore('cart', () => {
  // STATE (like ref() in components)
  const items = ref([])
  const couponCode = ref('')
  const isOpen = ref(false)

  // GETTERS (like computed() in components)
  const itemCount = computed(() =>
    items.value.reduce((sum, item) => sum + item.quantity, 0)
  )

  const subtotal = computed(() =>
    items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
  )

  const discount = computed(() =>
    couponCode.value === 'SAVE10' ? subtotal.value * 0.1 : 0
  )

  const total = computed(() => subtotal.value - discount.value)

  const isEmpty = computed(() => items.value.length === 0)

  // ACTIONS (methods)
  function addItem(product) {
    const existing = items.value.find(i => i.id === product.id)
    if (existing) {
      existing.quantity++
    } else {
      items.value.push({ ...product, quantity: 1 })
    }
  }

  function removeItem(productId) {
    items.value = items.value.filter(i => i.id !== productId)
  }

  function updateQuantity(productId, quantity) {
    const item = items.value.find(i => i.id === productId)
    if (item) {
      if (quantity <= 0) removeItem(productId)
      else item.quantity = quantity
    }
  }

  function clearCart() {
    items.value = []
    couponCode.value = &#039;&#039;
  }

  function toggleCart() {
    isOpen.value = !isOpen.value
  }

  // Async action
  async function checkout() {
    const orderData = { items: items.value, total: total.value }
    try {
      const res = await fetch(&#039;/api/orders&#039;, {
        method: &#039;POST&#039;,
        body: JSON.stringify(orderData),
        headers: { &#039;Content-Type&#039;: &#039;application/json&#039; }
      })
      const order = await res.json()
      clearCart()
      return order
    } catch (error) {
      console.error(&#039;Checkout failed:&#039;, error)
      throw error
    }
  }

  return {
    // State
    items, couponCode, isOpen,
    // Getters
    itemCount, subtotal, discount, total, isEmpty,
    // Actions
    addItem, removeItem, updateQuantity, clearCart, toggleCart, checkout
  }
})

6. Using the Store in Components

vue
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
<!-- Navbar.vue -->
<script setup>
import { useCartStore } from &#039;@/stores/cart'

const cart = useCartStore()
</script>
<template>
  <button @click="cart.toggleCart" class="cart-btn">
    🛒 Cart
    <span v-if="cart.itemCount > 0" class="badge">{{ cart.itemCount }}</span>
  </button>
</template>

<!-- ProductCard.vue -->
<script setup>
import { useCartStore } from &#039;@/stores/cart'

const cart = useCartStore()
const props = defineProps({ product: Object })
</script>
<template>
  <div class="product-card">
    <h3>{{ props.product.name }}</h3>
    <p>${{ props.product.price }}</p>
    <button @click="cart.addItem(props.product)">Add to Cart</button>
  </div>
</template>

<!-- CartSidebar.vue -->
<script setup>
import { useCartStore } from &#039;@/stores/cart'
import { storeToRefs } from &#039;pinia'

const cart = useCartStore()

// storeToRefs: destructure reactive state without losing reactivity
// (like toRefs for reactive objects)
const { items, total, isEmpty, isOpen } = storeToRefs(cart)

// Actions can be destructured directly (not reactive values)
const { removeItem, updateQuantity, clearCart } = cart
</script>

<template>
  <aside :class="[&#039;cart-sidebar', { open: isOpen }]">
    <h2>Shopping Cart</h2>
    <div v-if="isEmpty">Your cart is empty</div>
    <div v-else>
      <div v-for="item in items" :key="item.id" class="cart-item">
        <span>{{ item.name }}</span>
        <input
          type="number"
          :value="item.quantity"
          @change="updateQuantity(item.id, Number($event.target.value))"
          min="0"
        />
        <span>${{ (item.price * item.quantity).toFixed(2) }}</span>
        <button @click="removeItem(item.id)">✕</button>
      </div>

      <div class="coupon">
        <input v-model="cart.couponCode" placeholder="Coupon code" />
        <span v-if="cart.discount > 0" class="discount">
          -${{ cart.discount.toFixed(2) }}
        </span>
      </div>

      <div class="total">
        <strong>Total: ${{ total.toFixed(2) }}</strong>
      </div>

      <button @click="cart.checkout" class="checkout-btn">Checkout</button>
      <button @click="clearCart" class="clear-btn">Clear Cart</button>
    </div>
  </aside>
</template>

7. Common Mistakes

  • Destructuring store state without storeToRefs: const { items } = cart breaks reactivity. Use const { items } = storeToRefs(cart).
  • Calling actions without (): @click="cart.addItem" (missing args). Should be @click="cart.addItem(product)".

8. MCQs

Question 1

Pinia store ID (first arg to defineStore)?

Question 2

Store state is like?

Question 3

Store getters are like?

Question 4

storeToRefs() is used to?

Question 5

Can store actions be async?

Question 6

Where is Pinia registered?

Question 7

Access a store's state directly?

Question 8

Reset store to initial state?

Question 9

Pinia vs Vuex?

Question 10

Multiple components using same store see?

9. Interview Questions

  • Q: Why is Pinia preferred over Vuex in Vue 3?
  • Q: What is storeToRefs and why is it needed?

10. Summary

Pinia provides global reactive state with a Composition API that feels exactly like components — ref() for state, computed() for getters, async functions for actions. The storeToRefs helper enables safe destructuring. Every component accessing the same store sees the same reactive state.

11. Next Chapter Recommendation

In Chapter 17: Vuex Fundamentals, we cover Vuex — the original Vue state manager — for projects using Vue 2 or legacy codebases still on Vuex.

Finish this Chapter

Save your progress on your learning path and prepare for coding interview challenges.

Discussion

Join the discussion

Log in or create a free account to participate.

Sort: ·