Vue State

Pinia

Pinia is the official Vue store: simple, type-safe and built on the Composition API. State, getters and actions in one place, with no providers or boilerplate.

intermediate12 min readUpdated Sep 15, 2026
cart.js
js
// stores/cart.js
import { defineStore } from "pinia";
import { ref, computed } from "vue";

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

  const total = computed(() =>
    items.value.reduce((sum, item) => sum + item.price, 0),
  );

  function add(item) {
    items.value.push(item);
  }

  return { items, total, add };
});
Maintained by
The Vue team
Built on
The Composition API
Stores
defineStore
State
ref / reactive
Derived
computed
Devtools
Time-travel support

Why it matters

Why Pinia replaced Vuex

Simple by design

A store is a function that returns state and actions. There are no mutations, modules or providers to learn.

Type-safe

Pinia is written with TypeScript in mind, so state and actions infer their types with almost no extra annotations.

Composition API native

Setup stores use ref, computed and functions directly, so store code looks like the rest of your components.

The big picture

The three parts of a Pinia store

State, getters and actions. In a setup store they are just refs, computeds and functions.

State

Data

Reactive values declared with ref or reactive and returned from the store.

Getters

Derive

Computed values derived from state, cached and shared across components.

Actions

Updates

Functions that change state, run async logic and can call other stores.

Pinia at a glance

The core of Pinia

defineStore

Create a store with a unique id and either a setup or options function.

State

Reactive values that hold the data your app shares.

Getters

Computed values derived from state, cached automatically.

Actions

Functions that update state and can be async.

Plugins

Add persistence, logging or other behaviour to every store.

Stores in components

Call the store hook and access its state directly, with no providers.

A short history

From Vuex to the official store

  1. 2019

    Pinia introduced

    A lighter, Composition API friendly store appears as an alternative to Vuex.

    19
  2. 2020

    Vuex 4

    Vuex ships for Vue 3, but its module system and mutations feel heavy.

    20
  3. 2022

    Official for Vue 3

    Pinia becomes the officially recommended store and is bundled with new Vue projects.

    22
  4. 2023

    Nuxt integration

    Nuxt 3 ships Pinia support and auto-imports stores.

    23
  5. Today

    The Vue default

    Pinia is the standard state solution for Vue and Nuxt applications.

    Today

The complete guide

Pinia: Everything you need to know

What is Pinia?

Pinia is the official state management library for Vue. It replaced Vuex as the recommended store in Vue 3 and is now the default in new Vue and Nuxt projects. It is small, type-safe and built directly on the Composition API.

The design is intentionally minimal. A store is a function that returns state, derived values and actions. There are no mutations, no module namespacing and no per-store provider. If you know ref, computed and functions, you already know most of Pinia.

Defining a store

A store is created with defineStore, which takes a unique id and a setup function.

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

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

  const total = computed(() =>
    items.value.reduce((sum, item) => sum + item.price, 0),
  );

  const count = computed(() => items.value.length);

  function add(item) {
    items.value.push(item);
  }

  function remove(id) {
    items.value = items.value.filter((item) => item.id !== id);
  }

  return { items, total, count, add, remove };
});

This is a setup store: ref becomes state, computed becomes a getter, and functions become actions. The unique id names the store in devtools and is required.

Pinia also supports an options store if you prefer the Vuex-like shape:

// stores/counter.js
export const useCounter = defineStore("counter", {
  state: () => ({ count: 0 }),
  getters: {
    double: (state) => state.count * 2,
  },
  actions: {
    increment() {
      this.count += 1;
    },
  },
});

Both forms are fully supported. Setup stores tend to fit better with TypeScript and the Composition API, while options stores can feel familiar to Vuex users.

Using a store in components

Call the store function to get the store instance. There is no provider to wrap around your components.

<!-- Cart.vue -->
<script setup>
import { storeToRefs } from "pinia";
import { useCartStore } from "@/stores/cart";

const cart = useCartStore();
const { items, total } = storeToRefs(cart);
</script>

<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      {{ item.name }} — {{ item.price }}
      <button @click="cart.remove(item.id)">Remove</button>
    </li>
  </ul>
  <p>Total: {{ total }}</p>
</template>

Reading cart.total directly in a template stays reactive. When you want to destructure state or getters while keeping reactivity, use storeToRefs, which converts them into refs. Actions can be destructured directly because they do not need reactivity.

Getters

Getters are computed values derived from state. They are cached and shared, so every component reading the same getter shares one calculation.

// stores/products.js
export const useProductsStore = defineStore("products", () => {
  const products = ref([]);
  const filter = ref("");

  const visible = computed(() =>
    products.value.filter((p) =>
      p.name.toLowerCase().includes(filter.value.toLowerCase()),
    ),
  );

  const inStock = computed(() =>
    visible.value.filter((p) => p.stock > 0),
  );

  return { products, filter, visible, inStock };
});

Reaching for watch to compute a value is a common mistake. If it can be derived from existing state, it should be a getter, exactly as you would use computed in a component.

Actions

Actions are functions that change state. They can be synchronous or async, and they can call other actions or even other stores.

// stores/users.js
export const useUsersStore = defineStore("users", () => {
  const users = ref([]);
  const loading = ref(false);
  const error = ref(null);

  async function fetchUsers() {
    loading.value = true;
    error.value = null;
    try {
      const res = await fetch("/api/users");
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      users.value = await res.json();
    } catch (err) {
      error.value = err.message;
    } finally {
      loading.value = false;
    }
  }

  return { users, loading, error, fetchUsers };
});

For server data, though, consider a data-fetching layer such as TanStack Query, which handles caching and invalidation. Pinia is best for client state: carts, filters, UI preferences, auth state and drafts.

Plugins and persistence

Pinia plugins run for every store and can add behaviour such as persistence, logging or resetting.

// persist.js
export function persistPlugin({ store }) {
  const saved = localStorage.getItem(store.$id);
  if (saved) store.$patch(JSON.parse(saved));

  store.$subscribe((_mutation, state) => {
    localStorage.setItem(store.$id, JSON.stringify(state));
  });
}

// main.js
const pinia = createPinia();
pinia.use(persistPlugin);
app.use(pinia);

The popular pinia-plugin-persistedstate does the same thing with options for which keys to store. Persistence is one of the main reasons to reach for a store instead of local component state.

Pinia with Nuxt

In Nuxt, install the official module and stores in stores/ are auto-imported.

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ["@pinia/nuxt"],
});
<!-- pages/cart.vue -->
<script setup>
const cart = useCartStore(); // auto-imported
</script>

Because Nuxt handles installation and server rendering, the same store works on the server and the client. See the Nuxt guide for how this fits with useFetch and server routes.

Best practices

  • Prefer setup stores for new code, especially with TypeScript.
  • Keep derived data in getters instead of watching state.
  • Use storeToRefs when destructuring state or getters.
  • Keep server data in a data-fetching layer, not in Pinia.
  • Use actions for anything that changes state, including async work.
  • Persist only the state that should survive a reload.
  • Name stores clearly by the domain they own.

Common mistakes

  • Destructuring state without storeToRefs and losing reactivity.
  • Using watch to compute a value that a getter should derive.
  • Putting fetched server data in the store and hand-managing loading flags.
  • Forgetting to install Pinia, so store calls fail at runtime.
  • Creating one enormous store instead of focused domain stores.
  • Mutating state from outside an action in a way that is hard to trace.

Where to go next

Pinia is the Vue equivalent of a small, modern store. Deepen your Vue knowledge, add Nuxt for server rendering and auto-imports, and compare the approach with React’s Context API, Zustand and Redux Toolkit. Then build a small store with a getter, an async action and persistence to see how little code it takes.

Defining a store

A setup store uses the same ref, computed and function syntax as your components, so there is less to learn.

Setup store
export const useCounter = defineStore(
  "counter",
  () => {
    const count = ref(0);
    const double = computed(
      () => count.value * 2,
    );
    const inc = () => count.value++;
    return { count, double, inc };
  },
);
Options store
export const useCounter = defineStore(
  "counter",
  {
    state: () => ({ count: 0 }),
    getters: {
      double: (s) => s.count * 2,
    },
    actions: {
      inc() { this.count++; },
    },
  },
);

Derived values

Use getters (computed) for values derived from state. Do not watch state just to store a calculated value.

Prefer
const total = computed(() =>
  items.value.reduce(
    (sum, i) => sum + i.price, 0,
  ),
);
Avoid
const total = ref(0);
watch(items, () => {
  total.value = items.value.reduce(
    (sum, i) => sum + i.price, 0,
  );
});

FAQ

Frequently asked questions

Keep learning

Related topics from the roadmap.

$ start learning

Ready to start learning Pinia?

Our interactive tutorial walks you through Pinia step by step — with quizzes and real code you can run in the browser.