Skip to content
Release notesPinia

Pinia 3 is out: what it means for your stores

Pinia 3.0.0 was published on 11 February, with @pinia/testing 1.0 and @pinia/nuxt 0.10 the same day. Most stores do not change. Your package.json does, and maybe a store definition or a plugin type.

vuejs.school team13 Feb 20255 min readPinia 3.0 · Vue 3.5
cartitemstotaladd()refsRef<items>Ref<total>

Pinia 3.0.0 reached npm on 11 February 2025. The release notes open with the one sentence that matters: "This version of Pinia has no new features, it drops support for Vue 2 and other deprecated APIs."

If your app runs Vue 3 and every defineStore call already takes the store id as its first argument, the upgrade is a version bump.

The four breaking changes

Besides dropping Vue 2, the release notes list four breaking changes. Two of them remove APIs that Pinia 2 had already marked deprecated, and only those two can touch application code.

The first is the object form of defineStore. For a long time you could pass the store id inside the options object. Pinia 2.2.8 marked that form deprecated in November 2024, and Pinia 3 removes it. The id is now always the first argument:

stores/cart.tsBreaks
import { defineStore } from 'pinia'

export const useCartStore = defineStore({
  id: 'cart',
  state: () => ({ items: [] as string[] }),
})
stores/cart.tsWorks
import { defineStore } from 'pinia'

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

Left as it is, the old form fails on Pinia 3 in two places. TypeScript reports Expected 2-3 arguments, but got 1, and at runtime the first call to useCartStore() throws a TypeError (in Chrome, on the dev server: "Cannot destructure property 'state' of 'options' as it is undefined").

The second is the PiniaStorePlugin type, an old alias for PiniaPlugin, the type of a Pinia plugin function. Rename it:

plugins/logger.ts
import type { PiniaPlugin } from 'pinia'

export const loggerPlugin: PiniaPlugin = ({ store }) => {
  store.$subscribe((mutation) => {
    console.log(`[${store.$id}] ${mutation.type}`)
  })
}

This plugin logs state changes with the store's id and the kind of change, such as direct or patch object. You register it with pinia.use(loggerPlugin), as before. Because the alias is only a type, a leftover PiniaStorePlugin shows up in the type check, not in the browser: has no exported member named 'PiniaStorePlugin'. Did you mean 'PiniaPlugin'?

The other two breaking changes are about the package itself, not its API:

  • Pinia now uses TypeScript's built-in Awaited type instead of its own copy, so it needs TypeScript 4.5 or newer (more on that below).
  • The package is published as type: module, which means its files are ES modules by default. The notes say it still ships CommonJS files too, so an old Jest setup or a Node script that uses require('pinia') keeps working.

What stays the same

Everything you use day to day: defineStore with the id as the first argument, setup stores and option stores, storeToRefs, $patch, $subscribe, $reset on option stores, plugins and the pinia.use() call. Option stores keep their state, getters and actions shape. Setup stores keep returning refs, computeds and functions from a function. Nothing about how a store is defined, read in a component or tested has changed. The store below is written for Pinia 2 and runs on Pinia 3 without a single edit:

stores/counter.ts
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const double = computed(() => count.value * 2)

  function increment() {
    count.value++
  }

  return { count, double, increment }
})

And so does the component that uses it:

components/Counter.vue
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'

const store = useCounterStore()
const { count, double } = storeToRefs(store)
</script>

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

storeToRefs turns count and double into refs, so they stay reactive after destructuring. It leaves out actions such as increment, which is why the button calls it on the store.

New minimum versions

The migration guide lists three version changes. Check the first two before you run the install command.

  • Vue 3 only. Vue 2 reached end of life at the end of 2023, and teams still on Vue 2.7 keep using Pinia 2, which the guide says is fine. On Vue 3, the package's peer dependency range asks for 3.5.11 or newer, the same floor as Pinia 2.3. Vue 3.5.13 is the current release, so a project updated in the last few months already meets it.
  • TypeScript 5. The release notes set the floor at 4.5, the version that added Awaited, and the migration guide asks for 5 or newer. Use 5. Any project created in the last year has it already.
  • Devtools API v7. Pinia now depends on version 7 of @vue/devtools-api, the package behind its Vue Devtools integration (Pinia 2 used version 6). npm installs it with Pinia, so there is nothing to add yourself.

Upgrading a plain Vue app

Run these from the project root:

npm install pinia@^3
npm install -D @pinia/testing@^1   # only if you use createTestingPinia
grep -rn "defineStore({" src
grep -rn "PiniaStorePlugin" src
  1. Install. The @pinia/testing line matters only if your tests use createTestingPinia. Version 1.0.0 came out the same day and asks for Pinia 3. Its changelog says the package had been stable for a long time and the new major number mostly marks that; the one change is a warning when createSpy is set up incorrectly.
  2. Search. The first grep finds any store that still passes id in an object; move the id to the first argument as shown above. The second finds plugin type annotations to rename.
  3. Type-check. If the project uses TypeScript, run its type check. In a project made with create-vue that is npm run type-check, which runs vue-tsc --build. A plain npx vue-tsc --noEmit checks nothing there, because the root tsconfig.json lists no files and only points to other configs. It does work in projects whose root tsconfig.json includes the source files.
  4. Test. Run your tests. If the searches and the type check came back clean, they should pass without changes, and the upgrade is finished.

Upgrading a Nuxt app

Nuxt projects get Pinia through the @pinia/nuxt module. Version 0.10.0 shipped with Pinia 3 and, according to its changelog, requires Vue 3 and Pinia 3. It also asks for Nuxt 3.15 or newer (0.9 accepted 3.13 and later). On an older Nuxt the module is skipped with a warning that it "is disabled due to incompatibility issues", so update Nuxt first. Then install both packages together:

npm install pinia@^3 @pinia/nuxt@^0.10

Your nuxt.config.ts does not change:

nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@pinia/nuxt'],
})

To find leftovers, run the two searches from the plain Vue steps on the folders that hold your stores and plugins (usually stores/ and plugins/) instead of src, and npx nuxi typecheck if you use TypeScript.

Stores in stores/ keep being auto-imported, useCounterStore() keeps working in pages and components, and server-side rendering keeps hydrating state the same way. The migration guide has one line for older projects: Nuxt 2 and Nuxt Bridge users keep using the old version of Pinia.

Upgrade now or wait

Upgrade now, with your next routine dependency bump. A major release with no new features and two removed APIs is the easiest kind to adopt, and staying close to the current major keeps future upgrades small. The first patch, 3.0.1, followed on 12 February and only stops devtools code from ending up in production builds, so there is no reason to wait for the dust to settle.

Wait if you have a Vue 2 codebase, a Nuxt 2 or Bridge codebase, or a TypeScript version older than 5 that you cannot move yet. Pinia 2.3.1, published on 20 January, keeps working on Vue 2.7 and Vue 3.5, so nothing breaks while you stay.

Check it stuck3 questions from the Your first store quiz.
Practice now
Was this helpful?

Keep reading