Vue.js Pinia

Pinia is the official state management library for Vue 3. It gives your application a central place to store data that multiple components need to share. Without Pinia, sharing data between components that are not parent-child requires complex prop chains or event buses. Pinia solves this cleanly.

The Problem Pinia Solves

Imagine a shopping app. The cart item count needs to show in the header, the checkout button, and the cart page. Each component lives in a different part of the component tree. Passing data between them through props and events becomes messy quickly.

Diagram: Without vs With Pinia

WITHOUT Pinia — prop drilling:
  App
  ├── Header (needs cartCount) ← must receive via prop
  ├── Main
  │   ├── ProductList
  │   │   └── ProductCard
  │   │       └── AddToCart (has the data) → emit up → up → up → App
  └── Checkout (needs cartCount) ← must receive via prop

Data must travel through every level in between.
This is called "prop drilling" and gets unmanageable.

WITH Pinia — shared store:
  App
  ├── Header (reads cartStore.count directly)
  ├── Main
  │   ├── ProductList
  │   │   └── ProductCard
  │   │       └── AddToCart (writes to cartStore directly)
  └── Checkout (reads cartStore.count directly)

Every component reads and writes to the store directly.
No passing through levels in between.

Installing Pinia

npm install pinia
// src/main.js
import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";

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

Creating a Store

A store is a file that defines shared state, getters (computed values), and actions (methods). Create a stores folder and add one file per store.

// src/stores/counter.js
import { defineStore } from "pinia";

export const useCounterStore = defineStore("counter", {
  // State — the reactive data
  state() {
    return {
      count: 0,
      title: "My Counter"
    };
  },

  // Getters — computed values based on state
  getters: {
    doubleCount(state) {
      return state.count * 2;
    }
  },

  // Actions — functions that change state
  actions: {
    increment() {
      this.count++;
    },
    decrement() {
      this.count--;
    },
    resetTo(value) {
      this.count = value;
    }
  }
});

Diagram: Store Structure

useCounterStore
┌──────────────────────────────────────────┐
│  STATE                                   │
│    count: 0                              │
│    title: "My Counter"                   │
│                                          │
│  GETTERS                                 │
│    doubleCount → count × 2               │
│                                          │
│  ACTIONS                                 │
│    increment()  → count++                │
│    decrement()  → count--                │
│    resetTo(n)   → count = n              │
└──────────────────────────────────────────┘

Using the Store in a Component

Import and call the store's setup function. The returned object gives you access to all state, getters, and actions.

<!-- CounterWidget.vue -->
<template>
  <div>
    <h3>{{ store.title }}</h3>
    <p>Count: {{ store.count }}</p>
    <p>Double: {{ store.doubleCount }}</p>
    <button @click="store.increment">+1</button>
    <button @click="store.decrement">-1</button>
    <button @click="store.resetTo(0)">Reset</button>
  </div>
</template>

<script setup>
import { useCounterStore } from "../stores/counter";

const store = useCounterStore();
</script>

Because Pinia state is reactive, whenever store.count changes (from any component), every component using that store updates automatically.

The Same Store in Multiple Components

<!-- HeaderBar.vue -->
<script setup>
import { useCounterStore } from "../stores/counter";
const counter = useCounterStore();
</script>

<template>
  <span>Count: {{ counter.count }}</span>
</template>
<!-- FooterStats.vue -->
<script setup>
import { useCounterStore } from "../stores/counter";
const counter = useCounterStore();
</script>

<template>
  <p>Total clicks: {{ counter.count }}</p>
</template>

Diagram: Shared Reactive State

Pinia Store: count = 5

HeaderBar  → reads count = 5
FooterStats → reads count = 5

User clicks +1 in CounterWidget:
  store.increment() → count becomes 6

Pinia Store: count = 6

HeaderBar  → auto-updates → shows 6
FooterStats → auto-updates → shows 6

Pinia with the Setup Store Syntax

Pinia also supports a Composition API style called the Setup Store. It uses ref, computed, and regular functions directly — the same style as <script setup>.

// src/stores/cart.js
import { defineStore } from "pinia";
import { ref, computed } from "vue";

export const useCartStore = defineStore("cart", () => {
  // State
  const items = ref([]);

  // Getter
  const totalItems = computed(() => items.value.length);
  const totalPrice = computed(() =>
    items.value.reduce((sum, item) => sum + item.price, 0)
  );

  // Actions
  function addItem(product) {
    items.value.push(product);
  }

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

  function clearCart() {
    items.value = [];
  }

  return { items, totalItems, totalPrice, addItem, removeItem, clearCart };
});
<!-- CartIcon.vue -->
<script setup>
import { useCartStore } from "../stores/cart";
const cart = useCartStore();
</script>

<template>
  <span>🛒 {{ cart.totalItems }} items — ${{ cart.totalPrice }}</span>
</template>

Comparing Store Formats

Diagram: Options Store vs Setup Store

Options Store:
  defineStore("id", {
    state()    { return { ... } }
    getters:   { ... }
    actions:   { ... }
  })
  
  Structured. Familiar to Options API users.

Setup Store:
  defineStore("id", () => {
    const x = ref(...)         ← state
    const y = computed(...)    ← getter
    function doSomething() {}  ← action
    return { x, y, doSomething }
  })
  
  Flexible. Familiar to Composition API users.

Persisting Store State

Pinia state resets when the page reloads. To keep data between reloads, save to localStorage inside actions and load from it in state().

export const useSettingsStore = defineStore("settings", {
  state() {
    const saved = localStorage.getItem("settings");
    return saved
      ? JSON.parse(saved)
      : { theme: "light", language: "en" };
  },
  actions: {
    setTheme(theme) {
      this.theme = theme;
      localStorage.setItem("settings", JSON.stringify(this.$state));
    }
  }
});

Pinia DevTools

Pinia integrates with the Vue DevTools browser extension. In the DevTools panel, you can:

  • Inspect every store's current state
  • Watch state values change in real time as you interact with the app
  • Track which actions were called and when
  • Time-travel — step backward through state changes

Summary

Pinia is Vue 3's official state management solution. It stores shared data in stores that any component can read and write directly, eliminating prop drilling. Define state (reactive data), getters (computed values), and actions (methods that change state) in a single store file per feature. Use the Options Store for familiar structure or the Setup Store for Composition API style. Any component that uses the store updates automatically when the store's state changes. Pinia is lightweight, intuitive, and fully integrated with Vue DevTools.

Leave a Comment

Your email address will not be published. Required fields are marked *