---
title: "Pinia 4: what changed and what to do · vuejs.school"
description: "Pinia 4 is ESM only, needs @vue/devtools-api installed next to it and explains its errors better. Your stores stay the same. Here is the upgrade."
canonical_url: "https://vuejs.school/community/blog/pinia-4-what-changed-and-what-to-do"
meta:
  "og:description": "Pinia 4 is ESM only, needs @vue/devtools-api installed next to it and explains its errors better. Your stores stay the same. Here is the upgrade."
  "og:image": "https://vuejs.school/_og/d/a_~UGluaWEgNDogd2hhdCBjaGFuZ2VkIGFuZCB3aGF0IHRvIGRvIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~UGluaWEgNDogd2hhdCBjaGFuZ2VkIGFuZCB3aGF0IHRvIGRv%2Cdescription_~UGluaWEgNCBpcyBFU00gb25seSwgbmVlZHMgQHZ1ZS9kZXZ0b29scy1hcGkgaW5zdGFsbGVkIG5leHQgdG8gaXQgYW5kIGV4cGxhaW5zIGl0cyBlcnJvcnMgYmV0dGVyLiBZb3VyIHN0b3JlcyBzdGF5IHRoZSBzYW1lLiBIZXJlIGlzIHRoZSB1cGdyYWRlLg%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA3IG1pbiByZWFk%2Ctint_peach%2Cp_Ii9jb21tdW5pdHkvYmxvZy9waW5pYS00LXdoYXQtY2hhbmdlZC1hbmQtd2hhdC10by1kbyI%2Cs_4D7Hxt_PZh-Xurpd.png"
  "og:image:alt": "Pinia 4: what changed and what to do · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "Pinia 4: what changed and what to do · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~UGluaWEgNDogd2hhdCBjaGFuZ2VkIGFuZCB3aGF0IHRvIGRvIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~UGluaWEgNDogd2hhdCBjaGFuZ2VkIGFuZCB3aGF0IHRvIGRv%2Cdescription_~UGluaWEgNCBpcyBFU00gb25seSwgbmVlZHMgQHZ1ZS9kZXZ0b29scy1hcGkgaW5zdGFsbGVkIG5leHQgdG8gaXQgYW5kIGV4cGxhaW5zIGl0cyBlcnJvcnMgYmV0dGVyLiBZb3VyIHN0b3JlcyBzdGF5IHRoZSBzYW1lLiBIZXJlIGlzIHRoZSB1cGdyYWRlLg%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA3IG1pbiByZWFk%2Ctint_peach%2Cp_Ii9jb21tdW5pdHkvYmxvZy9waW5pYS00LXdoYXQtY2hhbmdlZC1hbmQtd2hhdC10by1kbyI%2Cs_4D7Hxt_PZh-Xurpd.png"
  "twitter:image:alt": "Pinia 4: what changed and what to do · vuejs.school"
---

> Index of every page on this site: [llms.txt](https://vuejs.school/llms.txt)

## 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`/ `export` format). The CommonJS build (for `require()`) and the `index.cjs` entry are gone.
- **@vue/devtools-api is a peer.** Pinia 3 pulled in `@vue/devtools-api` 7 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** ( `null` and `undefined`) 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.
- **`piniaSymbol` is 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.
- `skipHydrate` is 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:

*src/components/CartLogger.vue*

```
<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.

*src/stores/profile.ts**Breaks***

```
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:

*src/stores/profile.ts**Works***

```
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](https://pinia.vuejs.org/ssr/nuxt.html) 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:

1. **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 with `ERR_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.
2. **A missing devtools API.** npm 7 and later and pnpm install required peers for you, but Yarn classic and npm with `--legacy-peer-deps` do not. Pinia imports `@vue/devtools-api` at 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 in `package.json` explicitly and the question never comes up.
3. **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 an `ERESOLVE` error 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.

**Check it stuck**3 questions from the Your first store quiz.

[**Practice now**](https://vuejs.school/learn/pinia/your-first-store?tab=quiz)

**Was this helpful?**

## **Keep reading**

[Tutorial · 6 min read**Upgrading a Pinia 3 project to Pinia 4, with the Nuxt and testing packages**](https://vuejs.school/community/blog/upgrading-a-pinia-3-project-to-pinia-4) [Deep dive · 5 min read**Why storeToRefs drops your actions**](https://vuejs.school/community/blog/why-storetorefs-drops-your-actions)

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://vuejs.school/#website","@type":"WebSite","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school/","publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/pinia-4-what-changed-and-what-to-do#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2026-07-16","description":"Pinia 4 is ESM only, needs @vue/devtools-api installed next to it and explains its errors better. Your stores stay the same. Here is the upgrade.","name":"Pinia 4: what changed and what to do","url":"https://vuejs.school/community/blog/pinia-4-what-changed-and-what-to-do","breadcrumb":{"@id":"https://vuejs.school/community/blog/pinia-4-what-changed-and-what-to-do#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/pinia-4-what-changed-and-what-to-do"]}],"primaryImageOfPage":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/#identity","@type":"Organization","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school","logo":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/community/blog/pinia-4-what-changed-and-what-to-do#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2026-07-16","description":"Pinia 4 is ESM only, needs @vue/devtools-api installed next to it and explains its errors better. Your stores stay the same. Here is the upgrade.","headline":"Pinia 4: what changed and what to do","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~UGluaWEgNDogd2hhdCBjaGFuZ2VkIGFuZCB3aGF0IHRvIGRvIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~UGluaWEgNDogd2hhdCBjaGFuZ2VkIGFuZCB3aGF0IHRvIGRv%2Cdescription_~UGluaWEgNCBpcyBFU00gb25seSwgbmVlZHMgQHZ1ZS9kZXZ0b29scy1hcGkgaW5zdGFsbGVkIG5leHQgdG8gaXQgYW5kIGV4cGxhaW5zIGl0cyBlcnJvcnMgYmV0dGVyLiBZb3VyIHN0b3JlcyBzdGF5IHRoZSBzYW1lLiBIZXJlIGlzIHRoZSB1cGdyYWRlLg%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA3IG1pbiByZWFk%2Ctint_peach%2Cp_Ii9jb21tdW5pdHkvYmxvZy9waW5pYS00LXdoYXQtY2hhbmdlZC1hbmQtd2hhdC10by1kbyI%2Cs_4D7Hxt_PZh-Xurpd.png","@type":["Article","BlogPosting"],"articleSection":["Release notes"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/pinia-4-what-changed-and-what-to-do#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/pinia-4-what-changed-and-what-to-do#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/pinia-4-what-changed-and-what-to-do#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"Pinia 4: what changed and what to do","position":2}]},{"@id":"https://vuejs.school/#logo","@type":"ImageObject","caption":"vuejs.school","contentUrl":"https://vuejs.school/apple-touch-icon.png","inLanguage":"en","url":"https://vuejs.school/apple-touch-icon.png"},{"@id":"https://vuejs.school/#/schema/image/1","@type":"ImageObject","contentUrl":"https://vuejs.school/_og/d/a_~UGluaWEgNDogd2hhdCBjaGFuZ2VkIGFuZCB3aGF0IHRvIGRvIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~UGluaWEgNDogd2hhdCBjaGFuZ2VkIGFuZCB3aGF0IHRvIGRv%2Cdescription_~UGluaWEgNCBpcyBFU00gb25seSwgbmVlZHMgQHZ1ZS9kZXZ0b29scy1hcGkgaW5zdGFsbGVkIG5leHQgdG8gaXQgYW5kIGV4cGxhaW5zIGl0cyBlcnJvcnMgYmV0dGVyLiBZb3VyIHN0b3JlcyBzdGF5IHRoZSBzYW1lLiBIZXJlIGlzIHRoZSB1cGdyYWRlLg%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA3IG1pbiByZWFk%2Ctint_peach%2Cp_Ii9jb21tdW5pdHkvYmxvZy9waW5pYS00LXdoYXQtY2hhbmdlZC1hbmQtd2hhdC10by1kbyI%2Cs_4D7Hxt_PZh-Xurpd.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~UGluaWEgNDogd2hhdCBjaGFuZ2VkIGFuZCB3aGF0IHRvIGRvIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~UGluaWEgNDogd2hhdCBjaGFuZ2VkIGFuZCB3aGF0IHRvIGRv%2Cdescription_~UGluaWEgNCBpcyBFU00gb25seSwgbmVlZHMgQHZ1ZS9kZXZ0b29scy1hcGkgaW5zdGFsbGVkIG5leHQgdG8gaXQgYW5kIGV4cGxhaW5zIGl0cyBlcnJvcnMgYmV0dGVyLiBZb3VyIHN0b3JlcyBzdGF5IHRoZSBzYW1lLiBIZXJlIGlzIHRoZSB1cGdyYWRlLg%2Ceyebrow_~UmVsZWFzZSBub3RlcyDCtyA3IG1pbiByZWFk%2Ctint_peach%2Cp_Ii9jb21tdW5pdHkvYmxvZy9waW5pYS00LXdoYXQtY2hhbmdlZC1hbmQtd2hhdC10by1kbyI%2Cs_4D7Hxt_PZh-Xurpd.png"}]}
```