State Management

This section documents the current Vue 3.x release line as published at the official Vue.js documentation, which is the reference these pages are written and verified against. No specific patch version is pinned. The Composition API with <script setup> is the authoring style used throughout; the Options API is shown only as a contrast.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, since Vue and its ecosystem iterate quickly.

This section’s bibliography lists the reference material consulted while preparing these pages.

State management is about state that several components need to read and change. Vue’s reactivity APIs can hold such state on their own for small cases; Pinia is the official library once it grows. The trade-offs are laid out in State Management.

A store from the reactivity APIs

A shared reactive object in a module is already a working store — any component that imports it sees the same instance and re-renders when it changes:

// stores/counter.js
import { reactive } from 'vue'

export const counter = reactive({
  count: 0,
  increment() {
    this.count++
  },
})
<script setup>
import { counter } from '@/stores/counter'
</script>

<template>
  <button @click="counter.increment()">{{ counter.count }}</button>
</template>

This works, but nothing stops a component from writing counter.count = -5 directly, there is no naming convention for many stores, no DevTools timeline, no plugin hook, and server-side rendering needs care to avoid sharing one instance across requests. Pinia adds all of that.

Global state vs. props and provide/inject

Reach for a store only for state that is genuinely app-wide — the signed-in user, a cart, feature flags. Prefer passing data down as props for parent-to-child flow, and Provide / Inject for handing a value to a deep subtree without a store. A store is the right tool when unrelated parts of the tree read and write the same data.

Pinia

Install Pinia and register it once:

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

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

defineStore: setup stores vs. option stores

defineStore(id, …​) returns a use function. A setup store is a function that looks like <script setup> — ref is state, computed is a getter, functions are actions:

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

export const useCartStore = defineStore('cart', () => {
  const items = ref([])
  const total = computed(() => items.value.reduce((s, i) => s + i.price, 0))

  function addItem(item) {
    items.value.push(item)
  }
  function clear() {
    items.value = []
  }

  return { items, total, addItem, clear }
})

An option store passes an object with state, getters, and actions instead — closer to Vuex and to the Options API:

export const useCartStore = defineStore('cart', {
  state: () => ({ items: [] }),
  getters: { total: (state) => state.items.reduce((s, i) => s + i.price, 0) },
  actions: {
    addItem(item) { this.items.push(item) },
    clear() { this.items = [] },
  },
})

state must be a function so each app (and each SSR request) gets a fresh object. See Defining a Store.

Using a store in a component

Call the use function inside setup. The returned store is reactive; access properties directly for templates, but use storeToRefs() to destructure state and getters without losing reactivity (actions can be destructured directly):

<script setup>
import { storeToRefs } from 'pinia'
import { useCartStore } from '@/stores/cart'

const cart = useCartStore()
const { items, total } = storeToRefs(cart)   // stay reactive
const { addItem, clear } = cart              // actions: plain to destructure
</script>

<template>
  <p>{{ items.length }} items — {{ total }}</p>
  <button @click="clear">Empty cart</button>
</template>

Mutating and observing a store

  • store.$patch(\{ …​ }) or store.$patch(state ⇒ \{ …​ }) — apply several state changes as one transaction (cheaper, and one DevTools entry).

  • store.$reset() — restore the initial state (option stores only; a setup store must define its own reset action).

  • store.$subscribe((mutation, state) ⇒ \{ …​ }) — run a callback after every state change, e.g. to persist to localStorage.

  • store.$onAction((\{ name, args, after, onError }) ⇒ \{ …​ }) — hook every action call, with after and onError for its outcome.

const cart = useCartStore()

cart.$patch((state) => {
  state.items.push(newItem)
  state.coupon = null
})

cart.$subscribe((_mutation, state) => {
  localStorage.setItem('cart', JSON.stringify(state.items))
})

See State and Actions.

Plugins

A Pinia plugin is a function registered with pinia.use(); what it returns is merged onto every store, and it can also wrap actions or add subscriptions. This is how persistence, router access, or shared HTTP clients are attached:

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

Plugins documents the full context object.

DevTools, HMR, and SSR

  • DevTools — with Vue DevTools installed, Pinia adds a panel with each store’s state, a timeline of patches and actions, and time-travel.

  • HMR — add import.meta.hot glue so editing a store file keeps its current state:

    if (import.meta.hot) {
      import.meta.hot.accept(acceptHMRUpdate(useCartStore, import.meta.hot))
    }
  • SSR — because state is a factory, each request gets its own stores; serialize pinia.state.value into the HTML and hydrate it on the client. See SSR and Server-Side Rendering.

A Pinia store box with state, getters, and actions compartments; one component reads a getter while another calls an action

Vuex is legacy

Vuex was the previous official store. It is in maintenance mode and the Vue team recommends Pinia — effectively Vuex 5 — for all new projects.

See also