Pinia, the official state library for Vue, published version 4.0.0 on 14 July, together with @pinia/nuxt 1.0.0 and @pinia/testing 2.0.0. Two patches followed: 4.0.1 the same evening and 4.0.2 on 15 July. The release notes say Pinia 4 contains only technically breaking changes: the package is ESM only, and @vue/devtools-api must now be installed alongside it.
That is good news for your code. defineStore, setup stores, getters, actions, $patch, plugins and storeToRefs all keep their API, and the stores you wrote for Pinia 3 work unchanged. For most Vite and Nuxt apps, the upgrade is a version bump plus one extra package. It needs more care in three cases: code that loads Pinia with require(), installs that skip peer dependencies, and libraries whose peer range still stops at Pinia 3.
What changed
The release notes and the published package.json show these changes. The first two are the breaking ones; the notes list the rest as features:
- ESM only. The package's one entry point is now an ES module (the
import/exportformat). The CommonJS build (forrequire()) and theindex.cjsentry are gone. - @vue/devtools-api is a peer. Pinia 3 pulled in
@vue/devtools-api7 as its own dependency. Pinia 4 upgrades to version 8 and lists it as a required peer dependency (a package you install yourself, next to the one that needs it), with the range^8.1.5. - Better errors. Errors and development warnings now go through nostics, a small library for structured diagnostics. Each message has a code, an explanation and a link to the docs, and most add a fix.
- storeToRefs skips nullish values (
nullandundefined) instead of crashing on them. More on that below. - New development checks. Pinia now warns when a plugin adds an object to a store that is neither a ref nor a reactive object, because
storeToRefs()ignores such values. piniaSymbolis exported, the injection key Pinia uses to provide itself to the app.
The bug fixes you may notice:
- Passing the same callback to
$subscribe()twice no longer creates two watchers. skipHydrateis respected on objects that are not plain objects.- Getters with a setter can be edited in the devtools.
- Hot module replacement keeps properties added to setup store refs at runtime.
The peer ranges also moved: Vue stays at ^3.5.11, and TypeScript, still optional, now needs 5.6 or later.
Errors that tell you the fix
The nostics change is the one you are most likely to notice. Pinia 3 printed short warnings. Pinia 4 prints a code, the reason, the fix and a link. Here is what happens when a component subscribes the same function twice to a cart store that holds an items array:
<script setup lang="ts">
import { useCartStore } from '../stores/cart'
const cart = useCartStore()
const log = () => console.log(cart.items.length)
cart.$subscribe(log)
cart.$subscribe(log)
// In development, the console shows:
// [PINIA_R1007] The same callback was passed to "$subscribe()" of store "cart" more than once.
// Subscriptions are deduplicated, so the duplicate is ignored.
// ├▶ fix: Subscribe each callback only once. If you need to resubscribe, call the
// returned function to remove the previous subscription first, or create a new function.
// ╰▶ see: https://pinia.vuejs.org/core-concepts/state.html#Subscribing-to-the-state
</script>
The codes are stable, so you can search for PINIA_R1007 and find the explanation. Pinia's source runs every one of these calls in development only, so production builds drop the messages and their catalogue.
@pinia/testing 2.0 uses nostics for the errors it throws. If you write createTestingPinia({ createSpy: vi.fn() }) by mistake, where createSpy: vi.fn belongs, the test fails with PINIA_TESTING_C0002: Invalid "createSpy" option. The error object carries the fix as well: pass the function itself, not a called spy.
storeToRefs now skips null and undefined
storeToRefs() turns a store's state and getters into refs you can destructure without losing reactivity. In Pinia 3 it crashed on one particular mistake: a setup store that returns a plain null or undefined instead of a ref.
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useProfileStore = defineStore('profile', () => {
const name = ref('Ada')
const avatar = null as string | null
return { name, avatar }
})
On Pinia 3.0.4, storeToRefs(useProfileStore()) throws TypeError: Cannot read properties of null (reading 'effect'). Pinia 4 skips the value and carries on, the same way both versions skip other plain values such as a string. That sounds like the end of the story. It is not. The returned object simply has no avatar key, while TypeScript still types it as Ref<string | null>. So const { avatar } = storeToRefs(profile) compiles, and avatar.value fails at runtime with Cannot read properties of undefined (reading 'value').
The value was never state in the first place. Pinia only treats the refs and reactive objects a setup store returns as state, so a plain value stays out of $state, out of the devtools and out of server-side hydration. An action that reassigns the local variable changes nothing the store exposes. The fix is the same on both majors:
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useProfileStore = defineStore('profile', () => {
const name = ref('Ada')
const avatar = ref<string | null>(null)
return { name, avatar }
})
Before you upgrade, search your setup stores for returned values that are not wrapped in ref(), reactive() or computed(). Each one is either a constant that belongs outside the store, or state that needs a ref.
How to upgrade
Start on a green test run with Pinia 3, so any failure afterwards is the upgrade's doing. Then, in a Vite app:
npm install pinia@^4.0.2 @vue/devtools-api@^8.1.5
npm install -D @pinia/testing@^2.0.1
Skip the second line if you do not use @pinia/testing. Its 2.0 release needs Pinia 4, is ESM only too, and keeps the createTestingPinia() options exactly as they were. Your main.ts with app.use(createPinia()) stays as it is.
In a Nuxt app, upgrade the module with it:
npm install pinia@^4.0.2 @pinia/nuxt@^1.0.0 @vue/devtools-api@^8.1.5
@pinia/nuxt 1.0 changes no options. Its peer range moves to pinia ^4.0.0, and it still declares support for Nuxt 3.15 and later as well as Nuxt 4. Your nuxt.config.ts keeps '@pinia/nuxt' in modules, and the Nuxt page of the Pinia docs now tells you to install @vue/devtools-api alongside pinia.
If you use Pinia Colada, in either kind of app, move it to 1.4.1 or later in the same step. It is the first Colada release whose peer range accepts Pinia 4, starting at 4.0.2.
Then run your type check, your tests and the dev server, and read the console. Any PINIA_ code that appears points at something worth fixing.
What can break
Three things, and each one shows up immediately:
- CommonJS consumers. Code that loads Pinia with
require()needs Node to load an ES module that way. Node 20.19 and later, and 22.12 and later, do this by default. Older versions stop withERR_REQUIRE_ESM. Vite, Vitest and Nuxt load ES modules natively and are not affected; a test runner configured for CommonJS, such as Jest in its default mode, is. - A missing devtools API. npm 7 and later and pnpm install required peers for you, but Yarn classic and npm with
--legacy-peer-depsdo not. Pinia imports@vue/devtools-apiat the top of its code, so without it the app fails at startup with an error saying the package cannot be found or resolved. Put it inpackage.jsonexplicitly and the question never comes up. - Libraries that still expect Pinia 3. Plugins and other packages can list Pinia as a peer. If one of them still accepts only
^3.0.0, npm stops the install with anERESOLVEerror naming it, and pnpm prints a peer warning. That package needs its own update before you ship.
Plain null or undefined values read through storeToRefs() are not on this list: Pinia 3 already threw on them, and Pinia 4 only moves the crash to the line that reads .value.
Upgrade now or wait
Upgrade now if your stack is Vite or Nuxt, your tests run on Vitest, and nothing you depend on pins Pinia 3. The work is two install lines and a search for plain values in setup stores, and the new messages pay for themselves the first time one fires.
Wait if a library you rely on has not widened its peer range yet, or if you must still require() Pinia on an older Node version. Pinia 3.0.4 keeps working in the meantime, and nothing in your stores has to change when you do move.