Pinia

A store holds state that many components share; Pinia is the store library the Vue team recommends, installed once on the app.

Syntaxconst useCartStore = defineStore('cart', optionsOrSetupFunction)

A store is an object that holds state outside the component tree, so any component can read it and change it. Props and events are enough while data travels one or two levels. When a header badge, a sidebar and a checkout page all need the same cart, threading it through every component in between stops being worth it.

Installing it

Pinia is a separate package: npm install pinia. You create one instance with createPinia() and hand it to the application with app.use(pinia). That instance is where every store's state actually lives.

Defining a store

defineStore() takes two arguments: a unique id string, and either an options object or a setup function. It returns a function that you call inside a component, named useCartStore by convention. Calling it twice returns the same store, not a second copy.

If you have seen Vuex

Pinia is the successor to Vuex, which is no longer actively developed. State, getters and actions carry over with the same meaning. Mutations are gone: an action changes state directly, so there is no commit and no dispatch. Modules are gone too, because every store is already its own module, which removes namespaced strings and this.$store.

What trips people up

Calling useCartStore() at the top level of a module runs it before app.use(pinia), when no Pinia instance is active yet. Call it inside a component, a composable, or an action.

Example

Example · vue
<script setup lang="ts">
import { useCartStore } from '@/stores/cart'

const cart = useCartStore()
</script>

<template>
  <button type="button" class="badge">
    Cart ({{ cart.count }})
  </button>
</template>

<style scoped>
.badge {
  font: inherit;
  padding: 0.25rem 0.75rem;
}
</style>

When to use it

  • Keeping the signed-in user available to the header, a profile page and a route guard without passing props through every layer.
  • Holding a shopping cart that a product card writes to and a checkout page reads.
  • Sharing UI state, such as whether the sidebar is collapsed, between components that are not related.

More examples

Installing Pinia on the app

One Pinia instance per application, installed before mount. Reach for this once, in the entry file. Every store you later define reads and writes its state through this instance, which is also why a store function called on import has nothing to attach to.

Example · ts
// src/main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)
const pinia = createPinia()

app.use(pinia)
app.mount('#app')

The store the component above reads

One file per store, one store per domain. The id 'cart' is what devtools labels and what plugins key on, so it has to be unique across the app. Any component can now call useCartStore() and see the same lines.

Example · ts
// src/stores/cart.ts
import { defineStore } from 'pinia'

interface Line {
  id: string
  quantity: number
  priceCents: number
}

export const useCartStore = defineStore('cart', {
  state: () => ({
    lines: [] as Line[],
  }),
  getters: {
    count: (state) => state.lines.reduce((n, line) => n + line.quantity, 0),
  },
  actions: {
    add(line: Line) {
      const found = this.lines.find((l) => l.id === line.id)
      if (found) {
        found.quantity += line.quantity
        return
      }
      this.lines.push(line)
    },
  },
})

The older Vuex shape, for recognition

Useful when you are reading an older project or a blog post. The two halves do the same job. In Pinia an action writes state directly and is called like a method, so the commit and dispatch indirection disappears.

Example · ts
// Vuex 4, the older form. You will meet it in existing codebases.
const store = createStore({
  state: () => ({ count: 0 }),
  mutations: {
    increment(state) {
      state.count++
    },
  },
  actions: {
    bump({ commit }) {
      commit('increment')
    },
  },
})
// store.commit('increment')
// store.dispatch('bump')

// The same store in Pinia: no mutations, no commit, no dispatch.
export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  actions: {
    increment() {
      this.count++
    },
    bump() {
      this.increment()
    },
  },
})
// const counter = useCounterStore()
// counter.increment()

Discussion

  • Be the first to comment on this lesson.