Vue Pinia

Manage shared Vue application state with focused, reactive stores.

Overview

Pinia is Vue's official state management library. It places shared state and related business logic in stores that components can access without passing props through many levels. Stores expose reactive state, derived getters, and actions and integrate with TypeScript and Vue Devtools.

Key concepts

  • State contains shared reactive data
  • Getters derive values from store state
  • Actions perform updates and business workflows
  • Separate stores by feature or domain

What is Pinia?

Pinia is a library for centralized application state. Instead of components keeping separate copies of the same cart, account, or preference data, they read and update one store.

Why do we need Pinia?

In an online store, adding a product must update the cart page, header badge, checkout total, and perhaps saved cart data. Passing the same state through several unrelated component levels creates prop drilling. A Pinia cart store gives every relevant component one consistent source.

When should you use Pinia?

  • State is needed by distant components
  • Several routes share the same data
  • Business logic should remain outside presentation components
  • State transitions need debugging and Devtools visibility
  • A feature benefits from one explicit source of truth

Keep local UI state—such as whether one component's menu is open—inside that component unless other areas genuinely need it.

Installing Pinia

Output
npm install pinia

Registering Pinia

Create one Pinia instance and install it before mounting the application.

JavaScript
import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";

const app = createApp(App);

app.use(createPinia());
app.mount("#app");

Creating an Options Store

defineStore receives a unique store ID and an object containing state, getters, and actions.

JavaScript
import { defineStore } from "pinia";

export const useCounterStore = defineStore("counter", {
  state: () => ({
    count: 0
  }),

  getters: {
    doubleCount: (state) => state.count * 2
  },

  actions: {
    increment() {
      this.count++;
    }
  }
});

Using a Store in a Component

Vue SFC
<script setup>
import { useCounterStore } from "@/stores/counter";

const counter = useCounterStore();
</script>

<template>
  <p>Count: {{ counter.count }}</p>
  <p>Double: {{ counter.doubleCount }}</p>
  <button @click="counter.increment">Increment</button>
</template>

Every component using this store sees the same reactive count and updates when an action changes it.

State

State is the store's source data. It can be read and directly assigned, updated through actions, reset with $reset in an Options Store, or changed in groups with $patch.

JavaScript
counter.count++;

counter.$patch({
  count: 10
});

counter.$reset();

Getters

Getters are store-level computed values. They derive information without duplicating it in state.

JavaScript
getters: {
  itemCount: (state) =>
    state.items.reduce((total, item) => total + item.quantity, 0),

  cartTotal: (state) =>
    state.items.reduce(
      (total, item) => total + item.price * item.quantity,
      0
    )
}

Actions

Actions contain state changes and business workflows. They can be synchronous or asynchronous and can call other stores.

JavaScript
actions: {
  addItem(product) {
    const item = this.items.find((entry) => entry.id === product.id);

    if (item) item.quantity++;
    else this.items.push({ ...product, quantity: 1 });
  },

  async loadCart() {
    const response = await fetch("/api/cart");
    if (!response.ok) throw new Error("Unable to load cart");
    this.items = await response.json();
  }
}

Setup Stores

A Setup Store uses Composition API primitives. Returned refs become state, computed values become getters, and functions become actions.

JavaScript
import { computed, ref } from "vue";
import { defineStore } from "pinia";

export const useCounterStore = defineStore("counter", () => {
  const count = ref(0);
  const doubleCount = computed(() => count.value * 2);

  function increment() {
    count.value++;
  }

  return { count, doubleCount, increment };
});

Destructuring Store State

Directly destructuring state from a store breaks its reactive connection. Use storeToRefs for state and getters; actions can be destructured normally.

JavaScript
import { storeToRefs } from "pinia";

const counter = useCounterStore();
const { count, doubleCount } = storeToRefs(counter);
const { increment } = counter;

Cart Store example

JavaScript
import { defineStore } from "pinia";

export const useCartStore = defineStore("cart", {
  state: () => ({ items: [] }),

  getters: {
    itemCount: (state) =>
      state.items.reduce((sum, item) => sum + item.quantity, 0),
    total: (state) =>
      state.items.reduce(
        (sum, item) => sum + item.price * item.quantity,
        0
      )
  },

  actions: {
    add(product) {
      const existing = this.items.find((item) => item.id === product.id);
      if (existing) existing.quantity++;
      else this.items.push({ ...product, quantity: 1 });
    }
  }
});

The header reads itemCount, product cards call add, the cart renders items, and checkout reads total. All use one reactive store.

State Persistence

Pinia state does not persist across a full page reload by default. Persistence requires deliberate storage code or a compatible plugin. Never store secrets or authoritative security data in browser storage.

Pinia and Server Data

A store can cache API results, but distinguish client-owned application state from server-owned data. Define refresh, error, loading, and invalidation behavior instead of assuming fetched data remains current forever.

Benefits of Pinia

  • Centralized shared state
  • Reactive updates across components
  • Less prop drilling
  • Clear state, getter, and action responsibilities
  • Vue Devtools integration
  • Strong TypeScript inference
  • Modular stores

Common beginner mistakes

  • Forgetting app.use(createPinia())
  • Putting every local value in a store
  • Creating one enormous unrelated store
  • Duplicating the same source data across stores
  • Directly destructuring state without storeToRefs
  • Putting business logic in every component
  • Assuming state persists automatically
  • Storing sensitive information in browser persistence

Best practices

  • Create stores around domains or features
  • Keep state minimal and derive values with getters
  • Place reusable business workflows in actions
  • Keep temporary component UI state local
  • Use storeToRefs when destructuring
  • Represent loading and error states
  • Avoid circular store dependencies
  • Test important actions independently
  • Persist only intentional non-sensitive state

Key takeaways

  • Pinia is Vue's official state library
  • Stores centralize shared reactive data
  • State stores source values
  • Getters derive cached values
  • Actions perform updates and asynchronous workflows
  • Options and Setup Store styles are supported
  • storeToRefs preserves reactivity during destructuring
  • Local state should remain local
  • Well-scoped stores make large Vue applications easier to maintain
Let's learn with DevBrainBox AI