
Pinia Skilld
- 28 installs
- 173 repo stars
- Updated May 5, 2026
- skilld-dev/vue-ecosystem-skills
Helps with ai & agent building tasks.
About
pinia-skilld is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- pinia-skilld
- AI & Agent Building
- AI-coding skill
Pinia Skilld by the numbers
- 28 all-time installs (skills.sh)
- +1 installs in the week ending Jul 20, 2026 (Skillselion tracking)
- Ranked #9,505 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 20, 2026 (Skillselion catalog sync)
npx skills add https://github.com/skilld-dev/vue-ecosystem-skills --skill pinia-skilldAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 28 |
|---|---|
| repo stars | ★ 173 |
| Last updated | May 5, 2026 |
| Repository | skilld-dev/vue-ecosystem-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Discussions Index
Help and Questions (20)
- #3071: @pinia/nuxt auto-import feature not working with nuxt v4 (+5) [answered] (2025-11-17)
- #2987: Issue happening when migrating from @pinia/nuxt 0.4.8 to 0.11.0 (+4) [answered] (2025-05-31)
- #3085: @pinia/nuxt - No Active Pinia - Advanced Implementation from another plugin (+1) [answered] (2025-12-08)
- #3050: How to type-safely handle specific action returns in $onAction? (+1) [answered] (2025-09-29)
- #3026: TypeScript shows incorrect ref type for setup store properties instead of auto-unwrapped type (+1) [answered] (2025-08-26)
- #3003: Pinia plugins not working inside tests (+1) [answered] (2025-07-09)
- #2972: State not updating in Unit Tests when it's set in onmounted (+1) [answered] (2025-04-14)
- #2964: Watching a shallowRef array that is inside a store, from a Vue component (+1) [answered] (2025-04-04)
- #2955: Reset Pinia (+1) [answered] (2025-03-25)
- #2917: Asynchronous initialization and data recovery (+1) [answered] (2025-02-21)
- #2903: Incompatibility Issues Nuxt3 (+1) [answered] (2025-01-30)
- #2901: Pinia module causing [500] internal server error after upgrading to Nuxt version to 3.15.x (+1) [answered] (2025-01-29)
- #2894: Initialization ordering: can I manually initialize Pinia instance before Vue app instance is created? (+1) [answered] (2025-01-22)
- #2889: useLocalStorage or useSessionStorage inside pinia store always rewrite refs to default using nuxt [answered] (2025-01-15)
- #3087: 解决 vue2.7使用 pnpm 和 pinia 2.x hasInjectionContext 报错 (+1) [answered] (2025-12-17)
- #3033: Cannot use Pinia stores in Nuxt layers: "getActivePinia()" was called but there was no active Pinia. Are you trying to use a store before calling "app.use(pinia)"? (+1) [answered] (2025-09-05)
- #3030: Cannot handle Promise Like type in $onAction callback (+1) [answered] (2025-09-02)
- #2968: Cannot view error page, hydrate of error object fails (+1) [answered] (2025-04-08)
- #2962: rstore + Pinia lead to Error: obj.hasOwnProperty is not a function ⁃ at shouldHydrate (+1) [answered] (2025-04-02)
- #2922: In Nuxt 4, should
stores/go insidesrcDirorrootDir? (+1) [answered] (2025-02-24)
useLocalStorage or useSessionStorage inside pinia store always rewrite refs to default using nuxt
Reproduction
https://codesandbox.io/p/devbox/yjgfrk
Steps to reproduce the bug
1. Go to sandbox 2. Edit refs using the inputs 3. Reload the preview
Expected behavior
It should persists data
Actual behavior
Inside a pinia store, firstly it has the good value then it initializes again and overrides with the default one.
Additional information
The goal I try to achieve is to persists data for a session and keep my store synced.
---
Accepted Answer
@posva [maintainer]:
You probably need skipHydrate
- https://pinia.vuejs.org/cookbook/composables.html
- https://masteringpinia.com/blog/my-top-5-tips-for-using-pinia
Initialization ordering: can I manually initialize Pinia instance before Vue app instance is created?
I'm migrating a legacy Vue 3 + Vuex app to Pinia, but I stuck in the initialization ordering. This is this the simplified example:
// main.ts
import App from './ui/App.vue'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { useMyStore } from './ui/store' // Vuex stores / Pinia stores
const pinia = createPinia()
const myStore = useMyStore()
const app = createApp(App) // App.vue -> import { apiManager } from 'singleton.ts' -> `apiManager` also uses `useMyStore()` in './ui/store'
app.use(pinia) // Too late... Can this Pinia instance be initialized earlier?
app.mount('#vue-app')App.vueusesapiManagerinsingleton.tsapiManagerinsingleton.tsusesuseMyStorein './ui/store'- (
apiManageris responsible to fe...
---
Accepted Answer
@posva [maintainer]:
See https://pinia.vuejs.org/core-concepts/outside-component-usage.html#Using-a-store-outside-of-a-component. In short, put your pinia in a different file and import it in those singletons and pass it explicitly to the useStore() functions.
// src/pinia.ts
export const pinia = createPinia()// src/singleton.ts
import { useStore } from './stores/store.ts
import { pinia } from './pinia.ts
useStore(pinia)Pinia module causing [500] internal server error after upgrading to Nuxt version to 3.15.x
Reproduction
https://stackblitz.com/edit/github-6p4rt1bq
Steps to reproduce the bug
No specific steps required as when the app starts I am getting 500 internal server error. In the console I am getting twice
[nuxt] [request error] [unhandled] [500] [🍍]: "getActivePinia()" was called but there was no active Pinia. Are you trying to use a store before calling "app.use(pinia)"?
See https://pinia.vuejs.org/core-concepts/outside-component-usage.html for help.
This will fail in production.Expected behavior
It shouldn't produce any errors as I am using just a plain starter template with npx nuxi@latest init <my-app> and a store file from the pinia documentation example:
export const useCounterStore = defineStore("counter", () => {
const count = ref(0)
const name = ref("Eduardo")
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
...
---
## Accepted Answer
**@posva** [maintainer]:
You are using a regular script instead of a setup one:
<script lang="ts" setup> const store = useCounterStore(); </script>
The issue you are falling is in the link mentioned in the warning, https://pinia.vuejs.org/core-concepts/outside-component-usage.htmlIncompatibility Issues Nuxt3
Reproduction
N/A
Steps to reproduce the bug
1. Create a nuxt3 project 2. install pinia and configure 4. run 'npm install' 5. run 'npm run dev'
Expected behavior
Ability to use pinia as expected in nuxt application.
Actual behavior
...
---
Accepted Answer
@posva [maintainer]:
Follow the guidelines when opening issues, thanks The warning tells you you are using an outdated version of Nuxt:
[nuxt] Nuxt version ^2.0.0 || >=3.13.0 is required but currently using 3.12.2**
Asynchronous initialization and data recovery
What do I do to make sure that the data is properly recovered before it's served out, that is, to make sure that the init function is executed and the init function might be an asynchronous function like reading data from indexDB.
export const useUserStore = defineStore('user', {
state: () => {
return {
token: null as DataWithExpires<string> | null,
}
},
actions:{
async init(){
// read from indexdb
}
}
}It would be too inelegant to check initialization every place where you read it
---
Accepted Answer
@posva [maintainer]:
Mount the app after the init is done:
const app = createApp()
app.use(pinia)
await useUserStore(pinia).init()
app.mount()This can also be done within App.vue or with a boolean that toggles the Root <RouterViev>
In Nuxt 4, should stores/ go inside srcDir or rootDir ?
In the doc, it says at the root of your repository, but I'm not sure if that is only for Nuxt 3, or also for Nuxt 4 new structure. So should I do ./stores or ./app/stores ? I suppose the later
---
Accepted Answer
@posva [maintainer]:
It should be app
Reset Pinia
Hey,
In short: can we reset the Pinia repo completely? I know that we can remove all data from the Pinia store, e.g. reset to initial values. But can we reinitiate the defineStore function after that, e.g. execute it again?
Thanks!
---
Accepted Answer
@posva [maintainer]:
You will need to dispose of all stores. Currently this is only doable through an internal pinia._s property. It can also be implemented by collecting all store references in a pinia plugin (no need of an internal property )
rstore + Pinia lead to Error: obj.hasOwnProperty is not a function ⁃ at shouldHydrate
Reproduction
https://codesandbox.io/p/github/gabrielstuff/rstore-pinia-demo/main?import=true
Steps to reproduce the bug
First, thanks for all the work on Pinia. The issue I face is as follow :
- i'm using rstore to call and manage api call
- i'm willing to use Pinia to manage state for other use.
I'm using the following in my rstore :
const headers = useRequestHeaders(['cookie'])
const { data: todos, error, loading, refresh } = await store.todo.queryMany({
fetchOptions: {
headers
}
})which looks like working perfectly. In a real life example the rstore is making authenticated call with an API.
Without Pinia everything is going fine. As soon as I add Pinia the following errors rised up :
...
---
Accepted Answer
@posva [maintainer]:
Fixed in fd6d969
Watching a shallowRef array that is inside a store, from a Vue component
Reproduction
https://play.pinia.vuejs.org/#__DEV__eNqNVd9v2jAQ/lesvBAmcFp12iQGiG7qw6atndq+NX0I8QFuE9uynYCE+N93sfMLWrE+YXLffXf3+fJlH1wrRcsCgkkwNanmyhIDtlAkS8R6FgfWxME8FjxXUluyJ9vEphtyICstcxIHmBkH33pxY6WGR3kPK9OhFBc8OcYVBh4qaAMa0EgU+RK0odYMYhGLVAqDzTjQrMWHwyayJ3UCUsz6dUN3HmK1WLh2wxo4IuGQzOZkHwtCKhKZAc3kOoyDXvqk5d0mhhSKJRZYHFR8hz6p53KJ9IMFGtz/SkwjfxUoPP6xkKsMEfiPkOmysFYKskgznr7iBXnihLFbR4m3dc0YSeoK08jjfa6a/+aonBO8N/E0UnWclOOV1MjqswkXTafIu28UJ4dDl3JK2cx4lvQI+S71NOqNHYwCvzjjPFH0xUiB6+pEjusAkky87NWzet8meIzcmYLJx0sttwY05sfBqIEucIMjBqWVMjPjRHGftrFWmUkUpUwgnkHGS00F2EioPHqTs/hCv9DLKOPLCAtFXDDYHZfBjDGD/CPsDXRxQS8/06+OtbyqSfOKtSI94KqgLNbgmq34+kSUVOaKZ6DvlOW4hkfiJFkmt7/cM6sLaFtMN5C+vvP8xex81381oHwlvvBtzCZ6DdaHbx5uYYfnNphLVmSIPhO8B3xFiqpHD/teCIZt93Cu25/ujrlYP5qbnQVhmqGqRp0aDu9k/nFm9K7dK3rVU7FzHpSwt...
---
Accepted Answer
@posva [maintainer]:
[You need deep: true](https://play.pinia.vuejs.org/#__DEV__eNqVVV1v2jAU/StWXggTJK06dRIDRDe10qa1naBvTR9CfAG3iW3ZTkBC/Pfd2PmCVmx7iuN77rmfOdl7N1IGRQ7eyBvrRDFpiAaTS5LGfD2JPKMjbxpxlkmhDNmTbWySDTmQlRIZiTz0jLyvHbs2QsGTmMNKtyjJOIuPcbmGRQmtQb0g5Hm2BKUDo3sRj3giuMZkLGjS4P1+bdmTygEpJt24vj33MVrEbbp+BRwQv08mU7KPOCEliUghSMXaj7ynx/nt3aLLMmrot7EmuaSxARp5Je1hgMEpgBwRo3IsoRvLhbBEwd/j3s0f78mijH7s8r/Rx6EbHo4KXwxkMkUPfCNkvMyNEZzMkpQlbzhSFyim9MGGwPneUEriKuI4dHjnK6e/GPbajqjTnHEoKzsphiuhkNV5E8brzJF3X8+IHA6tyyllXfNZ0iPkh9TjsFO2N/Dcqg2zWAavWnBccNv/qDIgychNpLyrNnSEx9CeA9DZcKnEVoNC/8gb1NAZ7nxIoTBCpHoYS+bcNsZIPQrDhHLEU0hZoQIOJuQyC9/5zK6D6+AyTNkyxEAh4xR2x2HQY0gh+xf2Gjq7CC4/B18sa3FVkWYla0l6wNXBthiNG7hi65OmJCKTLAX1KA3DDT1qTpymYvvT3pUb16SYbCB5++D+Ve9c1r8VYPsKlIjGZmK1BuPMt4sH2OG5MWaC5imizxjngF9PXuboYN9yTjHtDs5m+8POmPH1k77dGeC6LqpM1HbD4m2bv58pvU33KrjqdLHVKmxhI2oUVoyf6JrdJhS0ViE3NkP8jAaYDVuvQdnzkbL2MC+rgrCzbk7yGtGcdEP5vSqb3juhMY2MoEI2ccfu8vll6j+/OO0iZJXzpKyfNMKAXFUf6mqLOM0hkLne+Pex2QQJsNSdVMypy...
Cannot view error page, hydrate of error object fails
Reproduction
https://stackblitz.com/edit/bobbiegoede-nuxt-i18n-starter-ycvcel1p?file=nuxt.config.ts,stores%2Fhello.js
Steps to reproduce the bug
1. Create nuxt project 2. Install pinia 3. Go to an error page (which should be 404) 4. ???? 5. obj.hasOwnProperty is not a function
Expected behavior
Should display error template.
Actual behavior
Goes through error, tries to hydrate payload which should not be hydrated.
Additional information
This error started appearing after the latest nuxt update, consistently happens only after cleaning lockfile.
---
Accepted Answer
@posva [maintainer]:
Fixed in fd6d969
State not updating in Unit Tests when it's set in onmounted
Reproduction
https://codesandbox.io/p/github/Yodablues/test-pinia-not-updating/main?import=true
Steps to reproduce the bug
I've created a codesandbox showing how this doesn't seem to work correctly. In it, I have App.vue with a onBeforeMounted hook calling an action to set some state in pinia. Then in another lifecycle hook, we check the state and it is undefined. Am i just doing this test wrong?
1. Mount your component and have a lifecycle hook set to modify the pinia state. 2. in the next lifecycle hook, notice the state isn't updated.
Expected behavior
The pinia state should be updated.
Actual behavior
The state is not updated.
Additional information
_No response_
---
Accepted Answer
@posva [maintainer]:
You need to pass stubActions: false to your test. https://pinia.vuejs.org/cookbook/testing.html#Customizing-behavior-of-actions
Issue happening when migrating from @pinia/nuxt 0.4.8 to 0.11.0
Reproduction
Just updated the package and when ran npm run build it starts breaking
Steps to reproduce the bug
we are using @pinia/nuxt which we tried to upgrade from 0.4.8 to 0.11.0 and ran npm run build and all nuxt pages are now not working and giving below error, checked all the store configurations given in pinia docs but all in vein can any one help on this
`[request error] [unhandled] [GET] http://localhost:3000/__nuxt_error?error=true&url=%2Fen%2Fstock%2F&statusCode=500&statusMessage=Server+Error&message=Cannot+read+properties+of+undefined+(reading+%27default%27) TypeError: obj.hasOwnProperty is not a function at shouldHydrate (/home/anantsingh/Documents/BasParts/nuxter/.output/server/node_modules/pinia/dist/pinia.prod.cjs:232:40) ... 8 lines matching cause s...
---
Accepted Answer
this is fixed by install pinia it was not in package.json earlier https://pinia.vuejs.org/ssr/nuxt.html " If you notice that pinia is not installed, please install it manually with your package manager"
pnpm add piniaPinia plugins not working inside tests
Even the most basic plugin fails in a test environment:
it('tests', async () => {
const foo = vi.fn();
const pinia = createPinia();
pinia.use(({ store }) => foo(store.$id));
setActivePinia(pinia);
const useTestStore = defineStore('store', { state: () => ({ foo: 1 }) });
const store = useTestStore();
expect(foo).toHaveBeenCalled();
});
Here is a minimal reproduction: https://stackblitz.com/edit/github-ofdqvmtu?file=test.test.ts
---
Accepted Answer
import { it, expect, vi } from 'vitest';
import { createPinia, setActivePinia, defineStore } from 'pinia';
import { createApp, defineComponent, h, KeepAlive } from 'vue';
it('tests', async () => {
const foo = vi.fn();
const pinia = createPinia().use(({ store }) => foo(store.$id));
setActivePinia(pinia);
createApp(defineComponent(() => () => h(KeepAlive, {}))).use(pinia); // this is needed for the store to work
const useTestStore = defineStore('store', { state: () => ({ foo: 1 }) });
const store = useTestStore();
expect(foo).toHaveBeenCalled();
});TypeScript shows incorrect ref type for setup store properties instead of auto-unwrapped type
Reproduction
https://pinia.vuejs.org/core-concepts/#Using-the-store
Steps to reproduce the bug
1. Create a Pinia store using setup syntax with defineStore('name', () => {}) 2. Define a ref inside the store: const count = ref(0) 3. Return the ref from the store: return { count } 4. Use the store in a Vue component: const store = useChatStore() 5. Access the property: store.count 6. Check TypeScript IntelliSense/hover information
Expected behavior
- TypeScript should show count: number (auto-unwrapped type)
- IntelliSense should provide correct type hints for the unwrapped value
- No TypeScript errors when using the property as a number
- Type should match runtime behavior
Actual behavior
...
---
Accepted Answer
@posva [maintainer]:
This was fixed in newer version, upgrade your versions.
Don't open issues that do not follow the guidelines, thank you.
Cannot handle Promise Like type in $onAction callback
when I use $onAction to add after action callback, which is using promise like, it doesn‘t work Example: App.vue <img width="433" height="188" alt="app" src="https://github.com/user-attachments/assets/fecb5ee1-f6ee-450f-b062-858000653ade" />
useTestStore.js <img width="776" height="781" alt="useStore" src="https://github.com/user-attachments/assets/73ece5b3-f9e4-4fe4-9a52-977ea0fb971f" />
maybe we can determine whether it is a Promise Like type,like use tool function function isPromiseLike (val) { return val && typeof val.then === 'function' } to replace the following judgement ` // if (isPromiseLike(ret)) if (ret instanceof Promise) { return ret .then((value) => { triggerSubscriptions(afterCallbackSet, value) ...
---
Accepted Answer
@posva [maintainer]:
My guess is that MyPromise is not properly extending the Promise class
Cannot use Pinia stores in Nuxt layers: "getActivePinia()" was called but there was no active Pinia. Are you trying to use a store before calling "app.use(pinia)"?
Reproduction
https://github.com/okainov/pinia-nuxt-layers-bug-repro
Summary
I'm observing the issue when stores are defined and working well in the base layer (as well as in .playground), it does NOT work at all with the app extending the layer. The error I'm getting is
>[]: "getActivePinia()" was called but there was no active Pinia. Are you trying to use a store before calling "app.use(pinia)"? See https://pinia.vuejs.org/core-concepts/outside-component-usage.html for help. This will fail in production.
Steps to reproduce the bug
...
---
Accepted Answer
While I played around with the reproducer for #3028, I was really surprised that it would work if I fix the imports thingy. So I digged deeper and seems like I found what was causing the issue
Problematic code:
import { defineStore } from 'pinia'
export const useTestStore = defineStore('test', () => {Working code:
//import { defineStore } from 'pinia'
export const useTestStore = defineStore('test', () => {Yes, if I remove import defineStore it starts working!
@posva do you really think it's not a bug? Did I miss some documentation saying "do not use imports in layers" or something similar?
How to type-safely handle specific action returns in $onAction?
Hey Pinia team!
I'm wondering about a TypeScript type inference challenge with $onAction.
When using something like:
assetStore.$onAction(({ name, after }) => {
after((ret) => {
if (name === "importAssetsFromPath") {
// How can I make TypeScript understand 'ret' is AssetImportResult here?
handleAssetsImported(ret as AssetImportResult);
}
});
}); Right now, I'm using a type assertion, but it feels a bit hacky.
Any elegant TypeScript solutions or patterns you'd recommend for this scenario?
Curious to hear your thoughts!
---
Accepted Answer
@posva [maintainer]:
Putting the if outside of the after should constrain the type as expected
@pinia/nuxt auto-import feature not working with nuxt v4
Reproduction
https://github.com/IvanKhartov/pinia-nuxt-bug
Steps to reproduce the bug
1. install nuxt app (v4) 2. install @pinia/nuxt module (v0.11.3) 3. add configuration into nuxt.config.ts - pinia: { storesDirs: ['./app/stores/**'] } 4. create folder app/stores, and any file with a simple store 5. use data from store in app.vue 6. run dev server (or build app)
Expected behavior
- no errors, and all works.
Actual behavior
- error:
useStore is not defined
Additional information
Note: for @pinia/nuxt v0.11.2 everything works fine
---
Accepted Answer
pinia: { storesDirs: ['./stores/**'] } just write it in config. I spend 2 hours to solve it). It works for nuxt 4
@pinia/nuxt - No Active Pinia - Advanced Implementation from another plugin
Reproduction
https://github.com/components-web-app/cwa-nuxt-module/tree/dev
Steps to reproduce the bug
When running my application I have a module, it adds plugin and it creates many stores that are needed.
https://github.com/components-web-app/cwa-nuxt-module/blob/dev/src/runtime/plugin.ts
It was all working fine until recent updates either within Nuxt or this module.
The CWA class creates a sub-class of storage where all the store definitions are created. Before they are created I've confirmed and there is an active pinia instance.
When I am trying to access the stores now, I get "getActivePinia()" was called but there was no active Pinia.
...
---
Accepted Answer
@posva [maintainer]:
See https://github.com/vuejs/pinia/pull/2915, you can always getActivePinia() in a different hook, save it, and then set it again after pinia nuxt but you will suffer from the same problem as #2915
解决 vue2.7使用 pnpm 和 pinia 2.x hasInjectionContext 报错
看了很多帖子,都说和vue2.7 不兼容,需要降级到2.0.x,但还是没有效果,pinia 2.x的最后更新版本也支持2.7,使用yarn可以运行,使用pnpm就报错
https://github.com/vuejs/pinia/blob/v2/packages/pinia/CHANGELOG.md
2.3.1 (2025-01-20)
Bug Fixes
types: support for Vue 2.7 (d14e1a7)
环境说明
package.json
"dependencies": {
"pinia": "2.3.1",
"vue": "2.7.16"
}
报错信息
报错关键字:export 'hasInjectionContext' (imported as 'hasInjectionContext') 报错信息: ...
---
Accepted Answer
@posva [maintainer]:
Because hasInjectionContext is only available through vue-demi.
Docs Index
- Getting Started: Install pinia with your favorite package manager:
- Pinia
- Introduction: <MasteringPiniaLink
href="https://play.gumlet.io/embed/651ecf274c2f339c6860e36b" mp-link="https://masteringpinia.com/lessons/the-what-and-why-of-st...
cookbook (11)
- Dealing with Composables: Composables are functions that leverage Vue Composition API to encapsulate and reuse stateful logic. Whether you write your own, you use external l...
- Composing Stores: Composing stores is about having stores that use each other, and this is supported in Pinia. There is one rule to follow:
- HMR (Hot Module Replacement): Pinia supports Hot Module replacement so you can edit your stores and interact with them directly in your app without reloading the page, allowing ...
- Cookbook
- Migrating from 0.0.7: The versions after 0.0.7: 0.1.0, and 0.2.0, came with a few big breaking changes. This guide helps you migrate whether you use Vue 2 or Vue 3. The ...
- Migrating from 0.x (v1) to v2: Starting at version 2.0.0-rc.4, pinia supports both Vue 2 and Vue 3! This means, all new updates will be applied to this version 2 so both Vue 2 an...
- Migrating from v2 to v3: Pinia v3 is a boring major release with no new features. It drops deprecated APIs and updates major dependencies. It only supports Vue 3. If you ar...
- Migrating from Vuex ≤4: Although the structure of Vuex and Pinia stores is different, a lot of the logic can be reused. This guide serves to help you through the process a...
- Usage without setup(): Pinia can be used even if you are not using the composition API (if you are using Vue <2.7, you still need to install the @vue/composition-api plug...
- Testing stores: <MasteringPiniaLink
href="https://play.gumlet.io/embed/65f9a9c10bfab01f414c25dc" title="Watch a free video of Mastering Pinia about testing stores" />
- VS Code Snippets: These are some snippets that I use in VS Code to make my life easier.
core-concepts (6)
- Actions: <MasteringPiniaLink
href="https://masteringpinia.com/lessons/the-3-pillars-of-pinia-actions" title="Learn all about actions in Pinia" />
- Getters: <MasteringPiniaLink
href="https://masteringpinia.com/lessons/the-3-pillars-of-pinia-getters" title="Learn all about getters in Pinia" />
- Defining a Store: <MasteringPiniaLink
href="https://play.gumlet.io/embed/651ecff2e4c322668b0a17af" mp-link="https://masteringpinia.com/lessons/quick-start-with-pinia...
- Using a store outside of a component: <MasteringPiniaLink
href="https://play.gumlet.io/embed/651ed1ec4c2f339c6860fd06" mp-link="https://masteringpinia.com/lessons/how-does-usestore-work...
- Plugins: <MasteringPiniaLink
href="https://masteringpinia.com/lessons/What-is-a-pinia-plugin" title="Learn all about Pinia plugins" />
- State: <MasteringPiniaLink
href="https://masteringpinia.com/lessons/the-3-pillars-of-pinia-state" title="Learn all about state in Pinia" />
ssr (2)
- Server Side Rendering (SSR): <MasteringPiniaLink
href="https://masteringpinia.com/lessons/ssr-friendly-state" title="Learn about SSR best practices" />
- Nuxt: <MasteringPiniaLink
href="https://masteringpinia.com/lessons/ssr-friendly-state" title="Learn about SSR best practices" />
zh/api/enums (1)
- Enumeration: MutationType %{#enumeration-mutationtype}%: API 文档 / pinia / MutationType
zh/api (1)
- API 文档 %{#api-documentation}%: API 文档
zh/api/interfaces (21)
- 接口:ModuleOptions: API 文档 / @pinia/nuxt / ModuleOptions
- 接口:TestingOptions %{#interface-testingoptions}%: API 文档 / @pinia/testing / TestingOptions
- 接口:TestingPinia %{#interface-testingpinia}%: API 文档 / @pinia/testing / TestingPinia
- 接口:_StoreOnActionListenerContext<Store, ActionName, A> %{#interface-storeonactionlistenercontext-store-actionname-a}%: API 文档 / pinia / StoreOnActionListenerContext
- 接口:_StoreWithState<Id, S, G, A> %{#interface-storewithstate-id-s-g-a}%: API 文档 / pinia / StoreWithState
- 接口:_SubscriptionCallbackMutationBase %{#interface-subscriptioncallbackmutationbase}%: API 文档 / pinia / SubscriptionCallbackMutationBase
- 接口:DefineSetupStoreOptions<Id, S, G, A> %{#interface-definesetupstoreoptions-id-s-g-a}%: API 文档 / pinia / DefineSetupStoreOptions
- 接口:DefineStoreOptions<Id, S, G, A> %{#interface-definestoreoptions-id-s-g-a}%: API 文档 / pinia / DefineStoreOptions
- 接口:DefineStoreOptionsBase<S, Store> %{#interface-definestoreoptionsbase-s-store}%: API 文档 / pinia / DefineStoreOptionsBase
- 接口:DefineStoreOptionsInPlugin<Id, S, G, A> %{#interface-definestoreoptionsinplugin-id-s-g-a}%: API 文档 / pinia / DefineStoreOptionsInPlugin
- 接口:MapStoresCustomization %{#interface-mapstorescustomization}%: API 文档 / pinia / MapStoresCustomization
- 接口:Pinia %{#interface-pinia}%: API 文档 / pinia / Pinia
- 接口:PiniaCustomProperties<Id, S, G, A> %{#interface-piniacustomproperties-id-s-g-a}%: API 文档 / pinia / PiniaCustomProperties
- 接口:PiniaCustomStateProperties<S> %{#interface-piniacustomstateproperties-s}%: API 文档 / pinia / PiniaCustomStateProperties
- 接口:PiniaPlugin %{#interface-piniaplugin}%: API 文档 / pinia / PiniaPlugin
- 接口:PiniaPluginContext<Id, S, G, A> %{#interface-piniaplugincontext-id-s-g-a}%: API 文档 / pinia / PiniaPluginContext
- 接口:StoreDefinition<Id, S, G, A> %{#interface-storedefinition-id-s-g-a}%: API 文档 / pinia / StoreDefinition
- 接口:StoreProperties<Id> %{#interface-storeproperties-id}%: API 文档 / pinia / StoreProperties
- 接口:SubscriptionCallbackMutationDirect %{#interface-subscriptioncallbackmutationdirect}%: API 文档 / pinia / SubscriptionCallbackMutationDirect
- 接口:SubscriptionCallbackMutationPatchFunction %{#interface-subscriptioncallbackmutationpatchfunction}%: API 文档 / pinia / SubscriptionCallbackMutationPatchFunction
- 接口:SubscriptionCallbackMutationPatchObject<S> %{#interface-subscriptioncallbackmutationpatchobject-s}%: API 文档 / pinia / SubscriptionCallbackMutationPatchObject
zh/api/modules (3)
- 模块: @pinia/nuxt %{#module-pinia-nuxt}%: API 文档 / @pinia/nuxt
- 模块:@pinia/testing %{#module-pinia-testing}%: API 文档 / @pinia/testing
- 模块:pinia %{#module-pinia}%: API 文档 / pinia
zh/cookbook (10)
- 处理组合式函数 %{#dealing-with-composables}%: 组合式函数是利用 Vue 组合式 API 来封装和复用有状态逻辑的函数。无论你是自己写,还是使用外部库,或者两者都有,你都可以在 pinia store 中充分发挥组合式函数的力量。
- 组合式 Store %{#composing-stores}%: 组合式 store 是可以相互使用,Pinia 当然也支持它。但有一个规则需要遵循:
- HMR (Hot Module Replacement) %{#hmr-hot-module-replacement}%: Pinia 支持热更新,所以你可以编辑你的 store,并直接在你的应用中与它们互动,而不需要重新加载页面,允许你保持当前的 state、并添加甚至删除 state、action 和 getter。
- 手册 %{#cookbook}%
- Migrating from 0.0.7 %{#migrating-from-0-0-7}%: The versions after 0.0.7: 0.1.0, and 0.2.0, came with a few big breaking changes. This guide helps you migrate whether you use Vue 2 or Vue 3. The ...
- 从 0.x (v1) 迁移至 v2 %{#migrating-from-0-x-v1-to-v2}%: 从 2.0.0-rc.4 版本开始,pinia 同时支持 Vue 2 和 Vue 3!这意味着,v2 版本的所有更新,将会让 Vue 2 和 Vue 3 的用户都受益。如果你使用的是 Vue 3,这对你来说没有任何改变,因为你已经在使用 rc 版本,你可以查看发布日志来了解所有更新的详细解释。...
- 从 Vuex ≤4 迁移 %{#migrating-from-vuex-≤4}%: 虽然 Vuex 和 Pinia store 的结构不同,但很多逻辑都可以复用。本指南的作用是帮助你完成迁移,并指出一些可能出现的常见问题。
- 不使用 setup() 的用法 %{#usage-without-setup}%: 即使你没有使用组合式 API,也可以使用 Pinia(如果你使用 Vue 2,你仍然需要安装 @vue/composition-api 插件)。虽然我们推荐你试着学习一下组合式 API,但对你和你的团队来说目前可能还不是时候,你可能正在迁移一个应用,或者有其他原因。你可以试试下面几个函数:
- store 测试 %{#testing-stores}%: <MasteringPiniaLink
href="https://play.gumlet.io/embed/65f9a9c10bfab01f414c25dc" title="Watch a free video of Mastering Pinia about testing stores" />
- VS Code 代码片段: 有一些代码片段可以让你在 VS Code 中更轻松地使用 Pinia。
zh/core-concepts (6)
- Action %{#actions}%: <MasteringPiniaLink
href="https://masteringpinia.com/lessons/the-3-pillars-of-pinia-actions" title="Learn all about actions in Pinia" />
- Getter %{#getters}%: <MasteringPiniaLink
href="https://masteringpinia.com/lessons/the-3-pillars-of-pinia-getters" title="Learn all about getters in Pinia" />
- 定义 Store %{#defining-a-store}%: <MasteringPiniaLink
href="https://play.gumlet.io/embed/651ecff2e4c322668b0a17af" mp-link="https://masteringpinia.com/lessons/quick-start-with-pinia...
- 在组件外使用 store %{#using-a-store-outside-of-a-component}%: <MasteringPiniaLink
href="https://play.gumlet.io/embed/651ed1ec4c2f339c6860fd06" mp-link="https://masteringpinia.com/lessons/how-does-usestore-work...
- 插件 %{#plugins}%: <MasteringPiniaLink
href="https://masteringpinia.com/lessons/What-is-a-pinia-plugin" title="Learn all about Pinia plugins" />
- State %{#state}%: <MasteringPiniaLink
href="https://masteringpinia.com/lessons/the-3-pillars-of-pinia-state" title="Learn all about state in Pinia" />
zh (3)
- 开始: 用你喜欢的包管理器安装 pinia:
- Pinia
- 简介 %{#introduction}%: <MasteringPiniaLink
href="https://play.gumlet.io/embed/651ecf274c2f339c6860e36b" mp-link="https://masteringpinia.com/lessons/the-what-and-why-of-st...
zh/ssr (2)
- 服务端渲染 (SSR) %{#server-side-rendering-ssr}%: <MasteringPiniaLink
href="https://masteringpinia.com/lessons/ssr-friendly-state" title="Learn about SSR best practices" />
- Nuxt %{#nuxt}%: <MasteringPiniaLink
href="https://masteringpinia.com/lessons/ssr-friendly-state" title="Learn about SSR best practices" />
Dealing with Composables
Composables are functions that leverage Vue Composition API to encapsulate and reuse stateful logic. Whether you write your own, you use external libraries or do both, you can fully use the power of Composables in your pinia stores.
Option Stores
<MasteringPiniaLink href="https://masteringpinia.com/lessons/using-composables-in-option-stores" title="Using Composables in Option Stores" />
When defining an option store, you can call a composable inside of the state property:
export const useAuthStore = defineStore('auth', {
state: () => ({
user: useLocalStorage('pinia/auth/login', 'bob'),
}),
})Keep in mind that you can only return writable state (e.g. a ref()). Here are some examples of composables that you can use:
<RuleKitLink />
- useLocalStorage
- useAsyncState
Here are some examples of composables that cannot be used in an option stores (but can be used with setup stores):
- useMediaControls: exposes functions
- useMemoryInfo: exposes readonly data
- useEyeDropper: exposes readonly data and functions
Setup Stores
<MasteringPiniaLink href="https://masteringpinia.com/lessons/using-composables-in-setup-stores" title="Using Composables in Setup Stores" />
On the other hand, when defining a setup store, you can use almost any composable since every property gets discerned into state, action, or getter:
import { defineStore } from 'pinia'
import { useMediaControls } from '@vueuse/core'
export const useVideoPlayer = defineStore('video', () => {
// we won't expose (return) this element directly
const videoElement = ref<HTMLVideoElement>()
const src = ref('/data/video.mp4')
const { playing, volume, currentTime, togglePictureInPicture } =
useMediaControls(videoElement, { src })
function loadVideo(element: HTMLVideoElement, src: string) {
videoElement.value = element
src.value = src
}
return {
src,
playing,
volume,
currentTime,
loadVideo,
togglePictureInPicture,
}
}):::warning Differently from regular state, ref<HTMLVideoElement>() contains a non-serializable reference to the DOM element. This is why we don't return it directly. Since it's client-only state, we know it won't be set on the server and will always start as undefined on the client. :::
SSR
When dealing with Server Side Rendering, you need to take care of some extra steps in order to use composables within your stores.
In Option Stores, you need to define a hydrate() function. This function is called when the store is instantiated on the client (the browser) when there is an initial state available at the time the store is created. The reason we need to define this function is because in such scenario, state() is not called.
import { defineStore } from 'pinia'
import { useLocalStorage } from '@vueuse/core'
export const useAuthStore = defineStore('auth', {
state: () => ({
user: useLocalStorage('pinia/auth/login', 'bob'),
}),
hydrate(state, initialState) {
// in this case we can completely ignore the initial state since we
// want to read the value from the browser
state.user = useLocalStorage('pinia/auth/login', 'bob')
},
})In Setup Stores, you need to use a helper named skipHydrate() on any state property that shouldn't be picked up from the initial state. Differently from option stores, setup stores cannot just _skip calling state()_, so we mark properties that cannot be hydrated with skipHydrate(). Note that this only applies to state properties:
import { defineStore, skipHydrate } from 'pinia'
import { useEyeDropper, useLocalStorage } from '@vueuse/core'
export const useColorStore = defineStore('colors', () => {
const { isSupported, open, sRGBHex } = useEyeDropper()
const lastColor = useLocalStorage('lastColor', sRGBHex)
// ...
return {
lastColor: skipHydrate(lastColor), // Ref<string>
open, // Function
isSupported, // boolean (not even reactive)
}
})Composing Stores
<RuleKitLink />
Composing stores is about having stores that use each other, and this is supported in Pinia. There is one rule to follow:
If two or more stores use each other, they cannot create an infinite loop through _getters_ or _actions_. They cannot both directly read each other's state in their setup function:
const useX = defineStore('x', () => {
const y = useY()
// ❌ This is not possible because y also tries to read x.name
y.name
function doSomething() {
// ✅ Read y properties in computed or actions
const yName = y.name
// ...
}
return {
name: ref('I am X'),
}
})
const useY = defineStore('y', () => {
const x = useX()
// ❌ This is not possible because x also tries to read y.name
x.name
function doSomething() {
// ✅ Read x properties in computed or actions
const xName = x.name
// ...
}
return {
name: ref('I am Y'),
}
})Nested Stores
Note that if one store uses another store, you can directly import and call the useStore() function within _actions_ and _getters_. Then you can interact with the store just like you would from within a Vue component. See Shared Getters and Shared Actions.
When it comes to _setup stores_, you can simply use one of the stores at the top of the store function:
import { defineStore } from 'pinia'
import { useUserStore } from './user'
import { apiPurchase } from './api'
export const useCartStore = defineStore('cart', () => {
const user = useUserStore()
const list = ref([])
const summary = computed(() => {
return `Hi ${user.name}, you have ${list.value.length} items in your cart. It costs ${price.value}.`
})
function purchase() {
return apiPurchase(user.id, list.value)
}
return { list, summary, purchase }
})Shared Getters
You can simply call useUserStore() inside a _getter_:
import { defineStore } from 'pinia'
import { useUserStore } from './user'
export const useCartStore = defineStore('cart', {
getters: {
summary(state) {
const user = useUserStore()
return `Hi ${user.name}, you have ${state.list.length} items in your cart. It costs ${state.price}.`
},
},
})Shared Actions
The same applies to _actions_:
import { defineStore } from 'pinia'
import { useUserStore } from './user'
import { apiOrderCart } from './api'
export const useCartStore = defineStore('cart', {
actions: {
async orderCart() {
const user = useUserStore()
try {
await apiOrderCart(user.token, this.items)
// another action
this.emptyCart()
} catch (err) {
displayError(err)
}
},
},
})Since actions can be asynchronous, make sure all of your `useStore()` calls appear before any `await`. Otherwise, this could lead to using the wrong pinia instance _in SSR apps_:
```js{7-8,11-13} import { defineStore } from 'pinia' import { useUserStore } from './user' import { apiOrderCart } from './api'
export const useCartStore = defineStore('cart', { actions: { async orderCart() { // ✅ call at the top of the action before any await const user = useUserStore()
try { await apiOrderCart(user.token, this.items) // ❌ called after an await statement const otherStore = useOtherStore() // another action this.emptyCart() } catch (err) { displayError(err) } }, }, })
HMR (Hot Module Replacement)
<RuleKitLink />
Pinia supports Hot Module replacement so you can edit your stores and interact with them directly in your app without reloading the page, allowing you to keep the existing state, add, or even remove state, actions, and getters.
At the moment, only Vite is officially supported but any bundler implementing the import.meta.hot spec should work (e.g. webpack seems to use import.meta.webpackHot instead of import.meta.hot). You need to add this snippet of code next to any store declaration. Let's say you have three stores: auth.js, cart.js, and chat.js, you will have to add (and adapt) this after the creation of the _store definition_:
// auth.js
import { defineStore, acceptHMRUpdate } from 'pinia'
export const useAuth = defineStore('auth', {
// options...
})
// make sure to pass the right store definition, `useAuth` in this case.
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useAuth, import.meta.hot))
}Cookbook
<RuleKitLink />
- Migrating from Vuex ≤4: A migration guide for converting Vuex ≤4 projects.
- HMR: How to activate hot module replacement and improve the developer experience.
- Testing Stores (WIP): How to unit test Stores and mock them in component unit tests.
- Composing Stores: How to cross use multiple stores. e.g. using the user store in the cart store.
- Options API: How to use Pinia without the composition API, outside of
setup(). - Migrating from 0.0.7: A migration guide with more examples than the changelog.
Migrating from 0.0.7
The versions after 0.0.7: 0.1.0, and 0.2.0, came with a few big breaking changes. This guide helps you migrate whether you use Vue 2 or Vue 3. The whole changelog can be found in the repository:
- For Pinia <= 1 for Vue 2
- For Pinia >= 2 for Vue 3
If you have questions or issues regarding the migration, feel free to open a discussion to ask for help.
No more store.state
You no longer access the store state via a state property, you can directly access any state property.
Given a store defined with:
const useStore({
id: 'main',
state: () => ({ count: 0 })
})Do
const store = useStore()
-store.state.count++
+store.count.++You can still access the whole store state with $state when needed:
-store.state = newState
+store.$state = newStateRename of store properties
All store properties (id, patch, reset, etc) are now prefixed with $ to allow properties defined on the store with the same names. Tip: you can refactor your whole codebase with F2 (or right-click + Refactor) on each of the store's properties
const store = useStore()
-store.patch({ count: 0 })
+store.$patch({ count: 0 })
-store.reset()
+store.$reset()
-store.id
+store.$idThe Pinia instance
It's now necessary to create a pinia instance and install it:
If you are using Vue 2 (Pinia <= 1):
import Vue from 'vue'
import { createPinia, PiniaVuePlugin } from 'pinia'
const pinia = createPinia()
Vue.use(PiniaVuePlugin)
new Vue({
el: '#app',
pinia,
// ...
})If you are using Vue 3 (Pinia >= 2):
import { createApp } from 'vue'
import { createPinia, PiniaVuePlugin } from 'pinia'
import App from './App.vue'
const pinia = createPinia()
createApp(App).use(pinia).mount('#app')The pinia instance is what holds the state and should be unique per application. Check the SSR section of the docs for more details.
SSR changes
The SSR plugin PiniaSsr is no longer necessary and has been removed. With the introduction of pinia instances, getRootState() is no longer necessary and should be replaced with pinia.state.value:
If you are using Vue 2 (Pinia <= 1):
// entry-server.js
-import { getRootState, PiniaSsr } from 'pinia',
+import { createPinia, PiniaVuePlugin } from 'pinia',
-// install plugin to automatically use correct context in setup and onServerPrefetch
-Vue.use(PiniaSsr);
+Vue.use(PiniaVuePlugin)
export default context => {
+ const pinia = createPinia()
const app = new Vue({
// other options
+ pinia
})
context.rendered = () => {
// pass state to context
- context.piniaState = getRootState(context.req)
+ context.piniaState = pinia.state.value
};
- return { app }
+ return { app, pinia }
}setActiveReq() and getActiveReq() have been replaced with setActivePinia() and getActivePinia() respectively. setActivePinia() can only be passed a pinia instance created with createPinia(). Note that most of the time you won't directly use these functions.
Migrating from 0.x (v1) to v2
<RuleKitLink />
Starting at version 2.0.0-rc.4, pinia supports both Vue 2 and Vue 3! This means, all new updates will be applied to this version 2 so both Vue 2 and Vue 3 users can benefit from it. If you are using Vue 3, this doesn't change anything for you as you were already using the rc and you can check the CHANGELOG for a detailed explanation of everything that changed. Otherwise, this guide is for you!
Deprecations
Let's take a look at all the changes you need to apply to your code. First, make sure you are already running the latest 0.x version to see any deprecations:
npm i 'pinia@^0.x.x'
# or with yarn
yarn add 'pinia@^0.x.x'If you are using ESLint, consider using this plugin to find all deprecated usages. Otherwise, you should be able to see them as they appear crossed. These are the APIs that were deprecated that were removed:
createStore()becomesdefineStore()- In subscriptions,
storeNamebecomesstoreId PiniaPluginwas renamedPiniaVuePlugin(Pinia plugin for Vue 2)$subscribe()no longer accepts a _boolean_ as second parameter, pass an object withdetached: trueinstead.- Pinia plugins no longer directly receive the
idof the store. Usestore.$idinstead.
Breaking changes
After removing these, you can upgrade to v2 with:
npm i 'pinia@^2.x.x'
# or with yarn
yarn add 'pinia@^2.x.x'And start updating your code.
Generic Store type
Added in 2.0.0-rc.0
Replace any usage of the type GenericStore with StoreGeneric. This is the new generic store type that should accept any kind of store. If you were writing functions using the type Store without passing its generics (e.g. Store<Id, State, Getters, Actions>), you should also use StoreGeneric as the Store type without generics creates an empty store type.
function takeAnyStore(store: Store) {} // [!code --]
function takeAnyStore(store: StoreGeneric) {} // [!code ++]
function takeAnyStore(store: GenericStore) {} // [!code --]
function takeAnyStore(store: StoreGeneric) {} // [!code ++]DefineStoreOptions for plugins
If you were writing plugins, using TypeScript, and extending the type DefineStoreOptions to add custom options, you should rename it to DefineStoreOptionsBase. This type will apply to both setup and options stores.
declare module 'pinia' {
export interface DefineStoreOptions<S, Store> { // [!code --]
export interface DefineStoreOptionsBase<S, Store> { // [!code ++]
debounce?: {
[k in keyof StoreActions<Store>]?: number
}
}
}PiniaStorePlugin was renamed
The type PiniaStorePlugin was renamed to PiniaPlugin.
import { PiniaStorePlugin } from 'pinia' // [!code --]
import { PiniaPlugin } from 'pinia' // [!code ++]
const piniaPlugin: PiniaStorePlugin = () => { // [!code --]
const piniaPlugin: PiniaPlugin = () => { // [!code ++]
// ...
}Note this change can only be done after upgrading to the latest version of Pinia without deprecations.
@vue/composition-api version
Since pinia now relies on effectScope(), you must use at least the version 1.1.0 of @vue/composition-api:
npm i @vue/composition-api@latest
# or with yarn
yarn add @vue/composition-api@latestwebpack 4 support
If you are using webpack 4 (Vue CLI uses webpack 4), you might encounter an error like this:
ERROR Failed to compile with 18 errors
error in ./node_modules/pinia/dist/pinia.mjs
Can't import the named export 'computed' from non EcmaScript module (only default export is available)This is due to the modernization of dist files to support native ESM modules in Node.js. Files are now using the extension .mjs and .cjs to let Node benefit from this. To fix this issue you have two possibilities:
- If you are using Vue CLI 4.x, upgrade your dependencies. This should include the fix below.
- If upgrading is not possible for you, add this to your
vue.config.js:
// vue.config.js
module.exports = {
configureWebpack: {
module: {
rules: [
{
test: /\.mjs$/,
include: /node_modules/,
type: 'javascript/auto',
},
],
},
},
}- If you are manually handling webpack, you will have to let it know how to handle
.mjsfiles:
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.mjs$/,
include: /node_modules/,
type: 'javascript/auto',
},
],
},
}Devtools
Pinia v2 no longer hijacks Vue Devtools v5, it requires Vue Devtools v6. Find the download link on the Vue Devtools documentation for the beta channel of the extension.
Nuxt
If you are using Nuxt, pinia has now its own dedicated Nuxt package . Install it with:
npm i @pinia/nuxt
# or with yarn
yarn add @pinia/nuxtAlso make sure to update your `@nuxtjs/composition-api` package.
Then adapt your nuxt.config.js and your tsconfig.json if you are using TypeScript:
// nuxt.config.js
module.exports {
buildModules: [
'@nuxtjs/composition-api/module',
'pinia/nuxt', // [!code --]
'@pinia/nuxt', // [!code ++]
],
}// tsconfig.json
{
"types": [
// ...
"pinia/nuxt/types" // [!code --]
"@pinia/nuxt" // [!code ++]
]
}It is also recommended to give the dedicated Nuxt section a read.
Migrating from v2 to v3
<RuleKitLink />
Pinia v3 is a _boring_ major release with no new features. It drops deprecated APIs and updates major dependencies. It only supports Vue 3. If you are using Vue 2, you can keep using v2. If you need help, book help with Pinia's author.
For most users, the migration should require no change. This guide is here to help you in case you encounter any issues.
Deprecations
defineStore({ id })
The defineStore() signature that accepts an object with an id property is deprecated. You should use the id parameter instead:
defineStore({ // [!code --]
id: 'storeName', // [!code --]
defineStore('storeName', { // [!code ++]
// ...
})PiniaStorePlugin
This deprecated type alias has been removed in favor of PiniaPlugin.
New versions
- Only Vue 3 is supported.
- TypeScript 5 or newer is required.
- The devtools API has been upgraded to v7.
Nuxt
The Nuxt module has been updated to support Nuxt 3. If you are using Nuxt 2 or Nuxt bridge, you can keep using the old version of Pinia.
Migrating from Vuex ≤4
Although the structure of Vuex and Pinia stores is different, a lot of the logic can be reused. This guide serves to help you through the process and point out some common gotchas that can appear.
Preparation
First, follow the Getting Started guide to install Pinia.
Restructuring Modules to Stores
Vuex has the concept of a single store with multiple _modules_. These modules can optionally be namespaced and even nested within each other.
The easiest way to transition that concept to be used with Pinia is that each module you used previously is now a _store_. Each store requires an id which is similar to a namespace in Vuex. This means that each store is namespaced by design. Nested modules can also each become their own store. Stores that depend on each other will simply import the other store.
How you choose to restructure your Vuex modules into Pinia stores is entirely up to you, but here is one suggestion:
<RuleKitLink />
# Vuex example (assuming namespaced modules)
src
└── store
├── index.js # Initializes Vuex, imports modules
└── modules
├── module1.js # 'module1' namespace
└── nested
├── index.js # 'nested' namespace, imports module2 & module3
├── module2.js # 'nested/module2' namespace
└── module3.js # 'nested/module3' namespace
# Pinia equivalent, note ids match previous namespaces
src
└── stores
├── index.js # (Optional) Initializes Pinia, does not import stores
├── module1.js # 'module1' id
├── nested-module2.js # 'nestedModule2' id
├── nested-module3.js # 'nestedModule3' id
└── nested.js # 'nested' idThis creates a flat structure for stores but also preserves the previous namespacing with equivalent ids. If you had some state/getters/actions/mutations in the root of the store (in the store/index.js file of Vuex) you may wish to create another store called something like root which holds all that information.
The directory for Pinia is generally called stores instead of store. This is to emphasize that Pinia uses multiple stores, instead of a single store in Vuex.
For large projects you may wish to do this conversion module by module rather than converting everything at once. You can actually mix Pinia and Vuex together during the migration so this approach can also work and is another reason for naming the Pinia directory stores instead.
Converting a Single Module
Here is a complete example of the before and after of converting a Vuex module to a Pinia store, see below for a step-by-step guide. The Pinia example uses an option store as the structure is most similar to Vuex:
// Vuex module in the 'auth/user' namespace
import { Module } from 'vuex'
import { api } from '@/api'
import { RootState } from '@/types' // if using a Vuex type definition
interface State {
firstName: string
lastName: string
userId: number | null
}
const storeModule: Module<State, RootState> = {
namespaced: true,
state: {
firstName: '',
lastName: '',
userId: null
},
getters: {
firstName: (state) => state.firstName,
fullName: (state) => `${state.firstName} ${state.lastName}`,
loggedIn: (state) => state.userId !== null,
// combine with some state from other modules
fullUserDetails: (state, getters, rootState, rootGetters) => {
return {
...state,
fullName: getters.fullName,
// read the state from another module named `auth`
...rootState.auth.preferences,
// read a getter from a namespaced module called `email` nested under `auth`
...rootGetters['auth/email'].details
}
}
},
actions: {
async loadUser ({ state, commit }, id: number) {
if (state.userId !== null) throw new Error('Already logged in')
const res = await api.user.load(id)
commit('updateUser', res)
}
},
mutations: {
updateUser (state, payload) {
state.firstName = payload.firstName
state.lastName = payload.lastName
state.userId = payload.userId
},
clearUser (state) {
state.firstName = ''
state.lastName = ''
state.userId = null
}
}
}
export default storeModule// Pinia Store
import { defineStore } from 'pinia'
import { useAuthPreferencesStore } from './auth-preferences'
import { useAuthEmailStore } from './auth-email'
import vuexStore from '@/store' // for gradual conversion, see fullUserDetails
interface State {
firstName: string
lastName: string
userId: number | null
}
export const useAuthUserStore = defineStore('authUser', {
// convert to a function
state: (): State => ({
firstName: '',
lastName: '',
userId: null
}),
getters: {
// firstName getter removed, no longer needed
fullName: (state) => `${state.firstName} ${state.lastName}`,
loggedIn: (state) => state.userId !== null,
// must define return type because of using `this`
fullUserDetails (state): FullUserDetails {
// import from other stores
const authPreferencesStore = useAuthPreferencesStore()
const authEmailStore = useAuthEmailStore()
return {
...state,
// other getters now on `this`
fullName: this.fullName,
...authPreferencesStore.$state,
...authEmailStore.details
}
// alternative if other modules are still in Vuex
// return {
// ...state,
// fullName: this.fullName,
// ...vuexStore.state.auth.preferences,
// ...vuexStore.getters['auth/email'].details
// }
}
},
actions: {
// no context as first argument, use `this` instead
async loadUser (id: number) {
if (this.userId !== null) throw new Error('Already logged in')
const res = await api.user.load(id)
this.updateUser(res)
},
// mutations can now become actions, instead of `state` as first argument use `this`
updateUser (payload) {
this.firstName = payload.firstName
this.lastName = payload.lastName
this.userId = payload.userId
},
// easily reset state using `$reset`
clearUser () {
this.$reset()
}
}
})Let's break the above down into steps:
1. Add a required id for the store, you may wish to keep this the same as the namespace before. It is also recommended to make sure the id is in _camelCase_ as it makes it easier to use with mapStores(). 2. Convert state to a function if it was not one already 3. Convert getters 1. Remove any getters that return state under the same name (eg. firstName: (state) => state.firstName), these are not necessary as you can access any state directly from the store instance 2. If you need to access other getters, they are on this instead of using the second argument. Remember that if you are using this then you will have to use a regular function instead of an arrow function. Also note that you will need to specify a return type because of TS limitations, see here for more details 3. If using rootState or rootGetters arguments, replace them by importing the other store directly, or if they still exist in Vuex then access them directly from Vuex 4. Convert actions 1. Remove the first context argument from each action. Everything should be accessible from this instead 2. If using other stores either import them directly or access them on Vuex, the same as for getters 5. Convert mutations 1. Mutations do not exist any more. These can be converted to actions instead, or you can just assign directly to the store within your components (eg. userStore.firstName = 'First') 2. If converting to actions, remove the first state argument and replace any assignments with this instead 3. A common mutation is to reset the state back to its initial state. This is built in functionality with the store's $reset method. Note that this functionality only exists for option stores.
As you can see most of your code can be reused. Type safety should also help you identify what needs to be changed if anything is missed.
Usage Inside Components
Now that your Vuex module has been converted to a Pinia store, any component or other file that uses that module needs to be updated too.
If you were using map helpers from Vuex before, it's worth looking at the Usage without setup() guide as most of those helpers can be reused.
If you were using useStore then instead import the new store directly and access the state on it. For example:
// Vuex
import { defineComponent, computed } from 'vue'
import { useStore } from 'vuex'
export default defineComponent({
setup () {
const store = useStore()
const firstName = computed(() => store.state.auth.user.firstName)
const fullName = computed(() => store.getters['auth/user/fullName'])
return {
firstName,
fullName
}
}
})// Pinia
import { defineComponent, computed } from 'vue'
import { useAuthUserStore } from '@/stores/auth-user'
export default defineComponent({
setup () {
const authUserStore = useAuthUserStore()
const firstName = computed(() => authUserStore.firstName)
const fullName = computed(() => authUserStore.fullName)
return {
// you can also access the whole store in your component by returning it
authUserStore,
firstName,
fullName
}
}
})Usage Outside Components
Updating usage outside of components should be simple as long as you're careful to _not use a store outside of functions_. Here is an example of using the store in a Vue Router navigation guard:
// Vuex
import vuexStore from '@/store'
router.beforeEach((to, from, next) => {
if (vuexStore.getters['auth/user/loggedIn']) next()
else next('/login')
})// Pinia
import { useAuthUserStore } from '@/stores/auth-user'
router.beforeEach((to, from, next) => {
// Must be used within the function!
const authUserStore = useAuthUserStore()
if (authUserStore.loggedIn) next()
else next('/login')
})More details can be found here.
Advanced Vuex Usage
In the case your Vuex store using some of the more advanced features it offers, here is some guidance on how to accomplish the same in Pinia. Some of these points are already covered in this comparison summary.
Dynamic Modules
There is no need to dynamically register modules in Pinia. Stores are dynamic by design and are only registered when they are needed. If a store is never used, it will never be "registered".
Hot Module Replacement
HMR is also supported but will need to be replaced, see the HMR Guide.
Plugins
If you use a public Vuex plugin then check if there is a Pinia alternative. If not you will need to write your own or evaluate whether the plugin is still necessary.
If you have written a plugin of your own, then it can likely be updated to work with Pinia. See the Plugin Guide.
Usage without setup()
Pinia can be used even if you are not using the composition API (if you are using Vue <2.7, you still need to install the @vue/composition-api plugin though). While we recommend you give the Composition API a try and learn it, it might not be the time for you and your team yet, you might be in the process of migrating an application, or any other reason. There are a few functions:
- mapStores
- mapState
- mapWritableState
- mapGetters (just for migration convenience, use
mapState()instead) - mapActions
<RuleKitLink />
Giving access to the whole store
If you need to access pretty much everything from the store, it might be too much to map every single property of the store... Instead you can get access to the whole store with mapStores():
import { mapStores } from 'pinia'
// given two stores with the following ids
const useUserStore = defineStore('user', {
// ...
})
const useCartStore = defineStore('cart', {
// ...
})
export default {
computed: {
// note we are not passing an array, just one store after the other
// each store will be accessible as its id + 'Store'
...mapStores(useCartStore, useUserStore)
},
methods: {
async buyStuff() {
// use them anywhere!
if (this.userStore.isAuthenticated()) {
await this.cartStore.buy()
this.$router.push('/purchased')
}
},
},
}By default, Pinia will add the "Store" suffix to the id of each store. You can customize this behavior by calling the setMapStoreSuffix():
import { createPinia, setMapStoreSuffix } from 'pinia'
// completely remove the suffix: this.user, this.cart
setMapStoreSuffix('')
// this.user_store, this.cart_store (it's okay, I won't judge you)
setMapStoreSuffix('_store')
export const pinia = createPinia()TypeScript
By default, all map helpers support autocompletion and you don't need to do anything. If you call setMapStoreSuffix() to change the "Store" suffix, you will need to also add it somewhere in a TS file or your global.d.ts file. The most convenient place would be the same place where you call setMapStoreSuffix():
import { createPinia, setMapStoreSuffix } from 'pinia'
setMapStoreSuffix('') // completely remove the suffix
export const pinia = createPinia()
declare module 'pinia' {
export interface MapStoresCustomization {
// set it to the same value as above
suffix: ''
}
}:::warning If you are using a TypeScript declaration file (like global.d.ts), make sure to import 'pinia' at the top of it to expose all existing types. :::
Testing stores
<MasteringPiniaLink href="https://play.gumlet.io/embed/65f9a9c10bfab01f414c25dc" title="Watch a free video of Mastering Pinia about testing stores" />
Stores will, by design, be used at many places and can make testing much harder than it should be. Fortunately, this doesn't have to be the case. We need to take care of three things when testing stores:
- The
piniainstance: Stores cannot work without it actions: most of the time, they contain the most complex logic of our stores. Wouldn't it be nice if they were mocked by default?- Plugins: If you rely on plugins, you will have to install them for tests too
Depending on what or how you are testing, we need to take care of these three things differently.
<RuleKitLink />
Unit testing a store
To unit test a store, the most important part is creating a pinia instance:
// stores/counter.spec.ts
import { setActivePinia, createPinia } from 'pinia'
import { useCounterStore } from '../src/stores/counter'
describe('Counter Store', () => {
beforeEach(() => {
// creates a fresh pinia and makes it active
// so it's automatically picked up by any useStore() call
// without having to pass it to it: `useStore(pinia)`
setActivePinia(createPinia())
})
it('increments', () => {
const counter = useCounterStore()
expect(counter.n).toBe(0)
counter.increment()
expect(counter.n).toBe(1)
})
it('increments by amount', () => {
const counter = useCounterStore()
counter.increment(10)
expect(counter.n).toBe(10)
})
})If you have any store plugins, there is one important thing to know: plugins won't be used until `pinia` is installed in an App. This can be solved by creating an empty App or a fake one:
import { setActivePinia, createPinia } from 'pinia'
import { createApp } from 'vue'
import { somePlugin } from '../src/stores/plugin'
// same code as above...
// you don't need to create one app per test
const app = createApp({})
beforeEach(() => {
const pinia = createPinia().use(somePlugin)
app.use(pinia)
setActivePinia(pinia)
})Unit testing components
This can be achieved with createTestingPinia(), which returns a pinia instance designed to help unit tests components.
Start by installing @pinia/testing:
npm i -D @pinia/testingAnd make sure to create a testing pinia in your tests when mounting a component:
import { mount } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
// import any store you want to interact with in tests
import { useSomeStore } from '@/stores/myStore'
const wrapper = mount(Counter, {
global: {
plugins: [createTestingPinia()],
},
})
const store = useSomeStore() // uses the testing pinia!
// state can be directly manipulated
store.name = 'my new name'
// can also be done through patch
store.$patch({ name: 'new name' })
expect(store.name).toBe('new name')
// actions are stubbed by default, meaning they don't execute their code by default.
// See below to customize this behavior.
store.someAction()
expect(store.someAction).toHaveBeenCalledTimes(1)
expect(store.someAction).toHaveBeenLastCalledWith()Initial State
You can set the initial state of all of your stores when creating a testing pinia by passing an initialState object. This object will be used by the testing pinia to _patch_ stores when they are created. Let's say you want to initialize the state of this store:
import { defineStore } from 'pinia'
const useCounterStore = defineStore('counter', {
state: () => ({ n: 0 }),
// ...
})Since the store is named _"counter"_, you need to add a matching object to initialState:
// somewhere in your test
const wrapper = mount(Counter, {
global: {
plugins: [
createTestingPinia({
initialState: {
counter: { n: 20 }, // start the counter at 20 instead of 0
},
}),
],
},
})
const store = useSomeStore() // uses the testing pinia!
store.n // 20Customizing behavior of actions
createTestingPinia stubs out all store actions unless told otherwise. This allows you to test your components and stores separately.
If you want to revert this behavior and normally execute your actions during tests, specify stubActions: false when calling createTestingPinia:
const wrapper = mount(Counter, {
global: {
plugins: [createTestingPinia({ stubActions: false })],
},
})
const store = useSomeStore()
// Now this call WILL execute the implementation defined by the store
store.someAction()
// ...but it's still wrapped with a spy, so you can inspect calls
expect(store.someAction).toHaveBeenCalledTimes(1)Selective action stubbing
Sometimes you may want to stub only specific actions while allowing others to execute normally. You can achieve this by passing an array of action names to the stubActions option:
// Only stub the 'increment' and 'reset' actions
const wrapper = mount(Counter, {
global: {
plugins: [
createTestingPinia({
stubActions: ['increment', 'reset'],
}),
],
},
})
const store = useSomeStore()
// These actions will be stubbed (not executed)
store.increment() // stubbed
store.reset() // stubbed
// Other actions will execute normally but still be spied
store.fetchData() // executed normally
expect(store.fetchData).toHaveBeenCalledTimes(1)For more complex scenarios, you can pass a function that receives the action name and store instance, and returns whether the action should be stubbed:
// Stub actions based on custom logic
const wrapper = mount(Counter, {
global: {
plugins: [
createTestingPinia({
stubActions: (actionName, store) => {
// Stub all actions that start with 'set'
if (actionName.startsWith('set')) return true
// Stub actions based on initial store state
if (store.isPremium) return false
return true
},
}),
],
},
})
const store = useSomeStore()
// Actions starting with 'set' are stubbed
store.setValue(42) // stubbed
// Other actions may execute based on the initial store state
store.fetchData() // executed or stubbed based on initial store.isPremium::: tip
- An empty array
[]means no actions will be stubbed (same asfalse) - The function is evaluated once at store setup time, receiving the store instance in its initial state
:::
You can also manually mock specific actions after creating the store:
const store = useSomeStore()
vi.spyOn(store, 'increment').mockImplementation(() => {})
// or if using testing pinia with stubbed actions
store.increment.mockImplementation(() => {})Mocking the returned value of an action
Actions are automatically spied but type-wise, they are still the regular actions. In order to get the correct type, we must implement a custom type-wrapper that applies the Mock type to each action. This type depends on the testing framework you are using. Here is an example with Vitest:
import type { Mock } from 'vitest'
import type { UnwrapRef } from 'vue'
import type { Store, StoreDefinition } from 'pinia'
function mockedStore<TStoreDef extends () => unknown>(
useStore: TStoreDef
): TStoreDef extends StoreDefinition<
infer Id,
infer State,
infer Getters,
infer Actions
>
? Store<
Id,
State,
Record<string, never>,
{
[K in keyof Actions]: Actions[K] extends (...args: any[]) => any
? // 👇 depends on your testing framework
Mock<Actions[K]>
: Actions[K]
}
> & {
[K in keyof Getters]: UnwrapRef<Getters[K]>
}
: ReturnType<TStoreDef> {
return useStore() as any
}This can be used in tests to get a correctly typed store:
import { mockedStore } from './mockedStore'
import { useSomeStore } from '@/stores/myStore'
const store = mockedStore(useSomeStore)
// typed!
store.someAction.mockResolvedValue('some value')If you are interesting in learning more tricks like this, you should check out the Testing lessons on Mastering Pinia.
Specifying the createSpy function
When using Jest, or vitest with globals: true, createTestingPinia automatically stubs actions using the spy function based on the existing test framework (jest.fn or vitest.fn). If you are not using globals: true or using a different framework, you'll need to provide a createSpy option:
::: code-group
``ts [vitest] // NOTE: not needed with globals: true` import { vi } from 'vitest'
createTestingPinia({ createSpy: vi.fn, })
import sinon from 'sinon'
createTestingPinia({ createSpy: sinon.spy, })
:::
You can find more examples in the tests of the testing package.
### Mocking getters
By default, any getter will be computed like regular usage but you can manually force a value by setting the getter to anything you want:
import { defineStore } from 'pinia' import { createTestingPinia } from '@pinia/testing'
const useCounterStore = defineStore('counter', { state: () => ({ n: 1 }), getters: { double: (state) => state.n * 2, }, })
const pinia = createTestingPinia() const counter = useCounterStore(pinia)
counter.double = 3 // 🪄 getters are writable only in tests
// set to undefined to reset the default behavior // @ts-expect-error: usually it's a number counter.double = undefined counter.double // 2 (=1 x 2)
### Pinia Plugins
If you have any pinia plugins, make sure to pass them when calling `createTestingPinia()` so they are properly applied. **Do not add them with `testingPinia.use(MyPlugin)`** like you would do with a regular pinia:
import { createTestingPinia } from '@pinia/testing' import { somePlugin } from '../src/stores/plugin'
// inside some test const wrapper = mount(Counter, { global: { plugins: [ createTestingPinia({ stubActions: false, plugins: [somePlugin], }), ], }, })
## E2E tests
When it comes to Pinia, you don't need to change anything for E2E tests, that's the whole point of these tests! You could maybe test HTTP requests, but that's way beyond the scope of this guide .
VS Code Snippets
<RuleKitLink />
These are some snippets that I use in VS Code to make my life easier.
Manage user snippets with <kbd>⇧ Shift</kbd>+<kbd>⌘ Command</kbd>+<kbd>P</kbd> / <kbd>⇧ Shift</kbd>+<kbd>⌃ Control</kbd>+<kbd>P</kbd> and then Snippets: Configure User Snippets.
{
"Pinia Options Store Boilerplate": {
"scope": "javascript,typescript",
"prefix": "pinia-options",
"body": [
"import { defineStore, acceptHMRUpdate } from 'pinia'",
"",
"export const use${TM_FILENAME_BASE/^(.*)$/${1:/pascalcase}/}Store = defineStore('$TM_FILENAME_BASE', {",
" state: () => ({",
" $0",
" }),",
" getters: {},",
" actions: {},",
"})",
"",
"if (import.meta.hot) {",
" import.meta.hot.accept(acceptHMRUpdate(use${TM_FILENAME_BASE/^(.*)$/${1:/pascalcase}/}Store, import.meta.hot))",
"}",
""
],
"description": "Bootstrap the code needed for a Vue.js Pinia Options Store file"
},
"Pinia Setup Store Boilerplate": {
"scope": "javascript,typescript",
"prefix": "pinia-setup",
"body": [
"import { defineStore, acceptHMRUpdate } from 'pinia'",
"",
"export const use${TM_FILENAME_BASE/^(.*)$/${1:/pascalcase}/}Store = defineStore('$TM_FILENAME_BASE', () => {",
" $0",
" return {}",
"})",
"",
"if (import.meta.hot) {",
" import.meta.hot.accept(acceptHMRUpdate(use${TM_FILENAME_BASE/^(.*)$/${1:/pascalcase}/}Store, import.meta.hot))",
"}",
""
],
"description": "Bootstrap the code needed for a Vue.js Pinia Setup Store file"
}
}Actions
<MasteringPiniaLink href="https://masteringpinia.com/lessons/the-3-pillars-of-pinia-actions" title="Learn all about actions in Pinia" />
Actions are the equivalent of methods in components. They can be defined with the actions property in defineStore() and they are perfect to define business logic:
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
actions: {
// since we rely on `this`, we cannot use an arrow function
increment() {
this.count++
},
randomizeCounter() {
this.count = Math.round(100 * Math.random())
},
},
})<RuleKitLink />
Like getters, actions get access to the _whole store instance_ through this with full typing (and autocompletion ) support. Unlike getters, `actions` can be asynchronous, you can await inside of actions any API call or even other actions! Here is an example using Mande. Note the library you use doesn't matter as long as you get a Promise. You could even use the native fetch function (browser only):
import { mande } from 'mande'
const api = mande('/api/users')
export const useUsers = defineStore('users', {
state: () => ({
userData: null,
// ...
}),
actions: {
async registerUser(login, password) {
try {
this.userData = await api.post({ login, password })
showTooltip(`Welcome back ${this.userData.name}``
You are also completely free to set whatever arguments you want and return anything. When calling actions, everything will be automatically inferred!
Actions are invoked like regular functions and methods:
<script setup> const store = useCounterStore() // call the action as a method of the store store.randomizeCounter() </script>
<template> <!-- Even on the template --> <button @click="store.randomizeCounter()">Randomize</button> </template>
## Accessing other stores actions
To consume another store, you can directly _use it_ inside of the _action_:
import { useAuthStore } from './auth-store'
export const useSettingsStore = defineStore('settings', { state: () => ({ preferences: null, // ... }), actions: { async fetchUserPreferences() { const auth = useAuthStore() if (auth.isAuthenticated) { this.preferences = await fetchPreferences() } else { throw new Error('User must be authenticated') } }, }, })
## Usage with the Options API
<VueSchoolLink
href="https://vueschool.io/lessons/access-pinia-actions-in-the-options-api"
title="Access Pinia Getters via the Options API"
/>
For the following examples, you can assume the following store was created:
// Example File Path: // ./src/stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', { state: () => ({ count: 0, }), actions: { increment() { this.count++ }, }, })
### With `setup()`
While Composition API is not for everyone, the `setup()` hook can make Pinia easier to work with while using the Options API. No extra map helper functions needed!
<script> import { useCounterStore } from '../stores/counter'
export default defineComponent({ setup() { const counterStore = useCounterStore()
return { counterStore } }, methods: { incrementAndPrint() { this.counterStore.increment() console.log('New Count:', this.counterStore.count) }, }, }) </script>
### Without `setup()`
If you would prefer not to use Composition API at all, you can use the `mapActions()` helper to map actions properties as methods in your component:
import { mapActions } from 'pinia' import { useCounterStore } from '../stores/counter'
export default { methods: { // gives access to this.increment() inside the component // same as calling from store.increment() ...mapActions(useCounterStore, ['increment']), // same as above but registers it as this.myOwnName() ...mapActions(useCounterStore, { myOwnName: 'increment' }), }, }
## Subscribing to actions
It is possible to observe actions and their outcome with `store.$onAction()`. The callback passed to it is executed before the action itself. `after` handles promises and allows you to execute a function after the action resolves. In a similar way, `onError` allows you to execute a function if the action throws or rejects. These are useful for tracking errors at runtime, similar to this tip in the Vue docs.
Here is an example that logs before running actions and after they resolve/reject.
const unsubscribe = someStore.$onAction( ({ name, // name of the action store, // store instance, same as someStore args, // array of parameters passed to the action after, // hook after the action returns or resolves onError, // hook if the action throws or rejects }) => { // a shared variable for this specific action call const startTime = Date.now() // this will trigger before an action on store is executed console.log(Start "${name}" with params [${args.join(', ')}].)
// this will trigger if the action succeeds and after it has fully run. // it waits for any returned promised after((result) => { console.log( Finished "${name}" after ${ Date.now() - startTime }ms.\nResult: ${result}. ) })
// this will trigger if the action throws or returns a promise that rejects onError((error) => { console.warn( Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}. ) }) } )
// manually remove the listener unsubscribe()
By default, _action subscriptions_ are bound to the component where they are added (if the store is inside a component's `setup()`). Meaning, they will be automatically removed when the component is unmounted. If you also want to keep them after the component is unmounted, pass `true` as the second argument to _detach_ the _action subscription_ from the current component:
<script setup> const someStore = useSomeStore()
// this subscription will be kept even after the component is unmounted someStore.$onAction(callback, true) </script>
Getters
<MasteringPiniaLink href="https://masteringpinia.com/lessons/the-3-pillars-of-pinia-getters" title="Learn all about getters in Pinia" />
Getters are exactly the equivalent of computed values for the state of a Store. They can be defined with the getters property in defineStore(). They receive the state as the first parameter to encourage the usage of arrow function:
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
getters: {
doubleCount: (state) => state.count * 2,
},
})<RuleKitLink />
Most of the time, getters will only rely on the state. However, they might need to use other getters. Because of this, we can get access to the _whole store instance_ through this when defining a regular function but it is necessary to define the type of the return type (in TypeScript). This is due to a known limitation in TypeScript and doesn't affect getters defined with an arrow function nor getters not using `this`:
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
getters: {
// automatically infers the return type as a number
doubleCount(state) {
return state.count * 2
},
// the return type **must** be explicitly set
doublePlusOne(): number {
// autocompletion and typings for the whole store ✨
return this.doubleCount + 1
},
},
})Then you can access the getter directly on the store instance:
<script setup>
import { useCounterStore } from './counterStore'
const store = useCounterStore()
</script>
<template>
<p>Double count is {{ store.doubleCount }}</p>
</template>Accessing other getters
As with computed properties, you can combine multiple getters. Access any other getter via this. In this scenario, you will need to specify a return type for the getter.
::: code-group
```ts [counterStore.ts] export const useCounterStore = defineStore('counter', { state: () => ({ count: 0, }), getters: { doubleCount(state) { return state.count * 2 }, doubleCountPlusOne(): number { return this.doubleCount + 1 }, }, })
// You can use JSDoc (https://jsdoc.app/tags-returns.html) in JavaScript export const useCounterStore = defineStore('counter', { state: () => ({ count: 0, }), getters: { // type is automatically inferred because we are not using this doubleCount: (state) => state.count 2, // here we need to add the type ourselves (using JSDoc in JS). We can also // use this to document the getter /*
- Returns the count value times two plus one.
*
- @returns {number}
*/ doubleCountPlusOne() { // autocompletion ✨ return this.doubleCount + 1 }, }, })
:::
## Passing arguments to getters
_Getters_ are just _computed_ properties behind the scenes, so it's not possible to pass any parameters to them. However, you can return a function from the _getter_ to accept any arguments:
export const useStore = defineStore('main', { getters: { getUserById: (state) => { return (userId) => state.users.find((user) => user.id === userId) }, }, })
and use in component:
<script setup> import { storeToRefs } from 'pinia' import { useUserListStore } from './store'
const userList = useUserListStore() const { getUserById } = storeToRefs(userList) // note you will have to use getUserById.value to access // the function within the <script setup> </script>
<template> <p>User 2: {{ getUserById(2) }}</p> </template>
Note that when doing this, **getters are not cached anymore**. They are simply functions you invoke. You can, however, cache some results inside of the getter itself, which is uncommon but should prove more performant:
export const useStore = defineStore('main', { getters: { getActiveUserById(state) { const activeUsers = state.users.filter((user) => user.active) return (userId) => activeUsers.find((user) => user.id === userId) }, }, })
## Accessing other stores getters
To use another store's getters, you can directly _use it_ inside of the _getter_:
import { useOtherStore } from './other-store'
export const useStore = defineStore('main', { state: () => ({ // ... }), getters: { otherGetter(state) { const otherStore = useOtherStore() return state.localData + otherStore.data }, }, })
## Usage with `setup()`
You can directly access any getter as a property of the store (exactly like state properties):
<script setup> const store = useCounterStore()
store.count = 3 store.doubleCount // 6 </script>
## Usage with the Options API
<VueSchoolLink
href="https://vueschool.io/lessons/access-pinia-getters-in-the-options-api"
title="Access Pinia Getters via the Options API"
/>
For the following examples, you can assume the following store was created:
// Example File Path: // ./src/stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', { state: () => ({ count: 0, }), getters: { doubleCount(state) { return state.count * 2 }, }, })
### With `setup()`
While Composition API is not for everyone, the `setup()` hook can make using Pinia easier to work with in the Options API. No extra map helper functions needed!
<script> import { useCounterStore } from '../stores/counter'
export default defineComponent({ setup() { const counterStore = useCounterStore()
// only return the whole store instead of destructuring return { counterStore } }, computed: { quadrupleCounter() { return this.counterStore.doubleCount * 2 }, }, }) </script>
This is useful while migrating a component from the Options API to the Composition API but **should only be a migration step**. Always try not to mix both API styles within the same component.
### Without `setup()`
You can use the same `mapState()` function used in the [previous section of state](./state.md#options-api) to map to getters:
import { mapState } from 'pinia' import { useCounterStore } from '../stores/counter'
export default { computed: { // gives access to this.doubleCount inside the component // same as reading from store.doubleCount ...mapState(useCounterStore, ['doubleCount']), // same as above but registers it as this.myOwnName ...mapState(useCounterStore, { myOwnName: 'doubleCount', // you can also write a function that gets access to the store double: (store) => store.doubleCount, }), }, }
Defining a Store
<MasteringPiniaLink href="https://play.gumlet.io/embed/651ecff2e4c322668b0a17af" mp-link="https://masteringpinia.com/lessons/quick-start-with-pinia" title="Get started with Pinia" />
Before diving into core concepts, we need to know that a store is defined using defineStore() and that it requires a unique name, passed as the first argument:
import { defineStore } from 'pinia'
// You can name the return value of `defineStore()` anything you want,
// but it's best to use the name of the store and surround it with `use`
// and `Store` (e.g. `useUserStore`, `useCartStore`, `useProductStore`)
// the first argument is a unique id of the store across your application
export const useAlertsStore = defineStore('alerts', {
// other options...
})This _name_, also referred to as _id_, is necessary and is used by Pinia to connect the store to the devtools. Naming the returned function _use..._ is a convention across composables to make its usage idiomatic.
defineStore() accepts two distinct values for its second argument: a Setup function or an Options object.
<RuleKitLink />
Option Stores
Similar to Vue's Options API, we can also pass an Options Object with state, actions, and getters properties.
```js {2-10} export const useCounterStore = defineStore('counter', { state: () => ({ count: 0, name: 'Eduardo' }), getters: { doubleCount: (state) => state.count * 2, }, actions: { increment() { this.count++ }, }, })
You can think of `state` as the `data` of the store, `getters` as the `computed` properties of the store, and `actions` as the `methods`.
Option stores should feel intuitive and simple to get started with.
## Setup Stores
There is also another possible syntax to define stores. Similar to the Vue Composition API's setup function, we can pass in a function that defines reactive properties and methods and returns an object with the properties and methods we want to expose.
export const useCounterStore = defineStore('counter', () => { const count = ref(0) const name = ref('Eduardo') const doubleCount = computed(() => count.value * 2) function increment() { count.value++ }
return { count, name, doubleCount, increment } })
In _Setup Stores_:
- `ref()`s become `state` properties
- `computed()`s become `getters`
- `function()`s become `actions`
Note that you **must** return **all state properties** in setup stores for Pinia to pick them up as state. In other words, you cannot have _private_ state properties in stores. Not returning all state properties or **making them readonly** will break [SSR](../cookbook/composables.md), devtools, and other plugins.
Setup stores bring a lot more flexibility than [Option Stores](#option-stores) as you can create watchers within a store and freely use any composable. However, keep in mind that using composables will get more complex when using SSR.
Setup stores are also able to rely on globally _provided_ properties like the Router or the Route. Any property provided at the App level can be accessed from the store using `inject()`, just like in components:
import { inject } from 'vue' import { useRoute } from 'vue-router' import { defineStore } from 'pinia'
export const useSearchFilters = defineStore('search-filters', () => { const route = useRoute() // this assumes app.provide('appProvided', 'value') was called const appProvided = inject('appProvided')
// ...
return { // ... } })
:::warning
Do not return properties like `route` or `appProvided` (from the example above) as they do not belong to the store itself and you can directly access them within components with `useRoute()` and `inject('appProvided')`.
:::
## What syntax should I pick?
As with Vue's Composition API and Options API, pick the one that you feel the most comfortable with. Both have their strengths and weaknesses. Options stores are easier to work with while Setup stores are more flexible and powerful. If you want to dive deeper into the differences, check the Option Stores vs Setup Stores chapter in Mastering Pinia.
## Using the store
We are _defining_ a store because the store won't be created until `use...Store()` is called within a component `<script setup>` (or within `setup()` **like all composables**):
<script setup> import { useCounterStore } from '@/stores/counter'
// access the store variable anywhere in the component ✨ const store = useCounterStore() </script>
:::tip
If you are not using `setup` components yet, [you can still use Pinia with _map helpers_](../cookbook/options-api.md).
:::
You can define as many stores as you want and **you should define each store in a different file** to get the most out of Pinia (like automatically allowing your bundler to code split and providing TypeScript inference).
Once the store is instantiated, you can access any property defined in `state`, `getters`, and `actions` directly on the store. We will look at these in detail in the next pages but autocompletion will help you.
Note that `store` is an object wrapped with `reactive`, meaning there is no need to write `.value` after getters but, like `props` in `setup`, **we cannot destructure it**:
<script setup> import { useCounterStore } from '@/stores/counter' import { computed } from 'vue'
const store = useCounterStore() // ❌ This won't work because it breaks reactivity // same as reactive: https://vuejs.org/guide/essentials/reactivity-fundamentals.html#limitations-of-reactive const { name, doubleCount } = store // [!code warning] name // will always be "Eduardo" // [!code warning] doubleCount // will always be 0 // [!code warning]
setTimeout(() => { store.increment() }, 1000)
// ✅ this one will be reactive // 💡 but you could also just use store.doubleCount directly const doubleValue = computed(() => store.doubleCount) </script>
## Destructuring from a Store
In order to extract properties from the store while keeping its reactivity, you need to use `storeToRefs()`. It will create refs for every reactive property. This is useful when you are only using state from the store but not calling any action. Note you can destructure actions directly from the store as they are bound to the store itself too:
<script setup> import { useCounterStore } from '@/stores/counter' import { storeToRefs } from 'pinia'
const store = useCounterStore() // name and doubleCount are reactive refs // This will also extract refs for properties added by plugins // but skip any action or non reactive (non ref/reactive) property const { name, doubleCount } = storeToRefs(store) // the increment action can just be destructured const { increment } = store </script>
Using a store outside of a component
<MasteringPiniaLink href="https://play.gumlet.io/embed/651ed1ec4c2f339c6860fd06" mp-link="https://masteringpinia.com/lessons/how-does-usestore-work" title="Using stores outside of components" />
Pinia stores rely on the pinia instance to share the same store instance across all calls. Most of the time, this works out of the box by just calling your useStore() function. For example, in setup(), you don't need to do anything else. But things are a bit different outside of a component. Behind the scenes, useStore() _injects_ the pinia instance you gave to your app. This means that if the pinia instance cannot be automatically injected, you have to manually provide it to the useStore() function. You can solve this differently depending on the kind of application you are writing.
<RuleKitLink />
Single Page Applications
If you are not doing any SSR (Server Side Rendering), any call of useStore() after installing the pinia plugin with app.use(pinia) will work:
import { useUserStore } from '@/stores/user'
import { createPinia } from 'pinia'
import { createApp } from 'vue'
import App from './App.vue'
// ❌ fails because it's called before the pinia is created
const userStore = useUserStore()
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
// ✅ works because the pinia instance is now active
const userStore = useUserStore()The easiest way to ensure this is always applied is to _defer_ calls of useStore() by placing them inside functions that will always run after pinia is installed.
Let's take a look at this example of using a store inside of a navigation guard with Vue Router:
import { createRouter } from 'vue-router'
const router = createRouter({
// ...
})
// ❌ Depending on the order of imports this will fail
const store = useUserStore()
router.beforeEach((to, from, next) => {
// we wanted to use the store here
if (store.isLoggedIn) next()
else next('/login')
})
router.beforeEach((to) => {
// ✅ This will work because the router starts its navigation after
// the router is installed and pinia will be installed too
const store = useUserStore()
if (to.meta.requiresAuth && !store.isLoggedIn) return '/login'
})SSR Apps
When dealing with Server Side Rendering, you will have to pass the pinia instance to useStore(). This prevents pinia from sharing global state between different application instances.
There is a whole section dedicated to it in the SSR guide, this is just a short explanation.
Plugins
<MasteringPiniaLink href="https://masteringpinia.com/lessons/What-is-a-pinia-plugin" title="Learn all about Pinia plugins" />
Pinia stores can be fully extended thanks to a low level API. Here is a list of things you can do:
- Add new properties to stores
- Add new options when defining stores
- Add new methods to stores
- Wrap existing methods
- Intercept actions and its results
- Implement side effects like Local Storage
- Apply only to specific stores
<RuleKitLink />
Plugins are added to the pinia instance with pinia.use(). The simplest example is adding a static property to all stores by returning an object:
import { createPinia } from 'pinia'
// add a property named `secret` to every store that is created
// after this plugin is installed this could be in a different file
function SecretPiniaPlugin() {
return { secret: 'the cake is a lie' }
}
const pinia = createPinia()
// give the plugin to pinia
pinia.use(SecretPiniaPlugin)
// in another file
const store = useStore()
store.secret // 'the cake is a lie'This is useful to add global objects like the router, modal, or toast managers.
Introduction
A Pinia plugin is a function that optionally returns properties to be added to a store. It takes one optional argument, a _context_:
export function myPiniaPlugin(context) {
context.pinia // the pinia created with `createPinia()`
context.app // the current app created with `createApp()`
context.store // the store the plugin is augmenting
context.options // the options object defining the store passed to `defineStore()`
// ...
}This function is then passed to pinia with pinia.use():
pinia.use(myPiniaPlugin)Plugins are only applied to stores created after the plugins themselves, and after `pinia` is passed to the app, otherwise they won't be applied.
Augmenting a Store
You can add properties to every store by simply returning an object of them in a plugin:
pinia.use(() => ({ hello: 'world' }))You can also set the property directly on the store but if possible use the return version so they can be automatically tracked by devtools:
pinia.use(({ store }) => {
store.hello = 'world'
})Any property _returned_ by a plugin will be automatically tracked by devtools so in order to make hello visible in devtools, make sure to add it to store._customProperties in dev mode only if you want to debug it in devtools:
// from the example above
pinia.use(({ store }) => {
store.hello = 'world'
// make sure your bundler handles this. webpack and vite should do it by default
if (process.env.NODE_ENV === 'development') {
// add any keys you set on the store
store._customProperties.add('hello')
}
})Note that every store is wrapped with reactive, automatically unwrapping any Ref (ref(), computed(), ...) it contains:
const sharedRef = ref('shared')
pinia.use(({ store }) => {
// each store has its individual `hello` property
store.hello = ref('secret')
// it gets automatically unwrapped
store.hello // 'secret'
// all stores are sharing the value `shared` property
store.shared = sharedRef
store.shared // 'shared'
})This is why you can access all computed properties without .value and why they are reactive.
Adding new state
If you want to add new state properties to a store or properties that are meant to be used during hydration, you will have to add it in two places:
- On the
storeso you can access it withstore.myState - On
store.$stateso it can be used in devtools and be serialized during SSR.
On top of that, you will certainly have to use a ref() (or other reactive API) in order to share the value across different accesses:
import { toRef, ref } from 'vue'
pinia.use(({ store }) => {
// to correctly handle SSR, we need to make sure we are not overriding an
// existing value
if (!store.$state.hasOwnProperty('hasError')) {
// hasError is defined within the plugin, so each store has their individual
// state property
const hasError = ref(false)
// setting the variable on `$state`, allows it be serialized during SSR
store.$state.hasError = hasError
}
// we need to transfer the ref from the state to the store, this way
// both accesses: store.hasError and store.$state.hasError will work
// and share the same variable
// See https://vuejs.org/api/reactivity-utilities.html#toref
store.hasError = toRef(store.$state, 'hasError')
// in this case it's better not to return `hasError` since it
// will be displayed in the `state` section in the devtools
// anyway and if we return it, devtools will display it twice.
})Note that state changes or additions that occur within a plugin (that includes calling store.$patch()) happen before the store is active and therefore do not trigger any subscriptions.
Resetting state added in plugins
By default, $reset() will not reset state added by plugins but you can override it to also reset the state you add:
import { toRef, ref } from 'vue'
pinia.use(({ store }) => {
// this is the same code as above for reference
if (!store.$state.hasOwnProperty('hasError')) {
const hasError = ref(false)
store.$state.hasError = hasError
}
store.hasError = toRef(store.$state, 'hasError')
// make sure to set the context (`this`) to the store
const originalReset = store.$reset.bind(store)
// override the $reset function
return {
$reset() {
originalReset()
store.hasError = false
},
}
})Adding new external properties
When adding external properties, class instances that come from other libraries, or simply things that are not reactive, you should wrap the object with markRaw() before passing it to pinia. Here is an example adding the router to every store:
import { markRaw } from 'vue'
// adapt this based on where your router is
import { router } from './router'
pinia.use(({ store }) => {
store.router = markRaw(router)
})Calling $subscribe inside plugins
You can use store.$subscribe and store.$onAction inside plugins too:
pinia.use(({ store }) => {
store.$subscribe(() => {
// react to store changes
})
store.$onAction(() => {
// react to store actions
})
})Adding new options
It is possible to create new options when defining stores to later on consume them from plugins. For example, you could create a debounce option that allows you to debounce any action:
defineStore('search', {
actions: {
searchContacts() {
// ...
},
},
// this will be read by a plugin later on
debounce: {
// debounce the action searchContacts by 300ms
searchContacts: 300,
},
})The plugin can then read that option to wrap actions and replace the original ones:
// use any debounce library
import debounce from 'lodash/debounce'
pinia.use(({ options, store }) => {
if (options.debounce) {
// we are overriding the actions with new ones
return Object.keys(options.debounce).reduce((debouncedActions, action) => {
debouncedActions[action] = debounce(
store[action],
options.debounce[action]
)
return debouncedActions
}, {})
}
})Note that custom options are passed as the 3rd argument when using the setup syntax:
defineStore(
'search',
() => {
// ...
},
{
// this will be read by a plugin later on
debounce: {
// debounce the action searchContacts by 300ms
searchContacts: 300,
},
}
)TypeScript
Everything shown above can be done with typing support, so you don't ever need to use any or @ts-ignore.
Typing plugins
A Pinia plugin can be typed as follows:
import { PiniaPluginContext } from 'pinia'
export function myPiniaPlugin(context: PiniaPluginContext) {
// ...
}Typing new store properties
When adding new properties to stores, you should also extend the PiniaCustomProperties interface.
import 'pinia'
import type { Router } from 'vue-router'
declare module 'pinia' {
export interface PiniaCustomProperties {
// by using a setter we can allow both strings and refs
set hello(value: string | Ref<string>)
get hello(): string
// you can define simpler values too
simpleNumber: number
// type the router added by the plugin above (#adding-new-external-properties)
router: Router
}
}It can then be written and read safely:
pinia.use(({ store }) => {
store.hello = 'Hola'
store.hello = ref('Hola')
store.simpleNumber = Math.random()
// @ts-expect-error: we haven't typed this correctly
store.simpleNumber = ref(Math.random())
})PiniaCustomProperties is a generic type that allows you to reference properties of a store. Imagine the following example where we copy over the initial options as $options (this would only work for option stores):
pinia.use(({ options }) => ({ $options: options }))We can properly type this by using the 4 generic types of PiniaCustomProperties:
import 'pinia'
declare module 'pinia' {
export interface PiniaCustomProperties<Id, S, G, A> {
$options: {
id: Id
state?: () => S
getters?: G
actions?: A
}
}
}:::tip When extending types in generics, they must be named exactly as in the source code. Id cannot be named id or I, and S cannot be named State. Here is what every letter stands for:
- S: State
- G: Getters
- A: Actions
- SS: Setup Store / Store
:::
Typing new state
When adding new state properties (to both, the store and store.$state), you need to add the type to PiniaCustomStateProperties instead. Differently from PiniaCustomProperties, it only receives the State generic:
import 'pinia'
declare module 'pinia' {
export interface PiniaCustomStateProperties<S> {
hello: string
}
}Typing new creation options
When creating new options for defineStore(), you should extend the DefineStoreOptionsBase. Differently from PiniaCustomProperties, it only exposes two generics: the State and the Store type, allowing you to limit what can be defined. For example, you can use the names of the actions:
import 'pinia'
declare module 'pinia' {
export interface DefineStoreOptionsBase<S, Store> {
// allow defining a number of ms for any of the actions
debounce?: Partial<Record<keyof StoreActions<Store>, number>>
}
}:::tip
There is also a StoreGetters type to extract the _getters_ from a Store type. You can also extend the options of _setup stores_ or _option stores_ only by extending the types DefineStoreOptions and DefineSetupStoreOptions respectively.
:::
Nuxt
When using pinia alongside Nuxt, you will have to create a Nuxt plugin first. This will give you access to the pinia instance:
```ts{14-16} // plugins/myPiniaPlugin.ts import { PiniaPluginContext } from 'pinia'
function MyPiniaPlugin({ store }: PiniaPluginContext) { store.$subscribe((mutation) => { // react to store changes console.log([🍍 ${mutation.storeId}]: ${mutation.type}.) })
// Note this has to be typed if you are using TS return { creationTime: new Date() } }
export default defineNuxtPlugin(({ $pinia }) => { $pinia.use(MyPiniaPlugin) })
::: info
The above example is using TypeScript, you have to remove the type annotations `PiniaPluginContext` and `Plugin` as well as their imports if you are using a `.js` file.
:::
## Existing plugins
You can check existing Pinia plugins on GitHub with the topic _pinia-plugin_.
State
<MasteringPiniaLink href="https://masteringpinia.com/lessons/the-3-pillars-of-pinia-state" title="Learn all about state in Pinia" />
The state is, most of the time, the central part of your store. People often start by defining the state that represents their app. In Pinia the state is defined as a function that returns the initial state. This allows Pinia to work in both Server and Client Side.
import { defineStore } from 'pinia'
export const useStore = defineStore('storeId', {
// arrow function recommended for full type inference
state: () => {
return {
// all these properties will have their type inferred automatically
count: 0,
name: 'Eduardo',
isAdmin: true,
items: [],
hasChanged: true,
}
},
}):::tip
In order for Vue to properly detect state, you must declare every state piece in state, even if its initial value is undefined.
:::
<RuleKitLink />
TypeScript
You don't need to do much in order to make your state compatible with TS: make sure strict, or at the very least, noImplicitThis, is enabled and Pinia will infer the type of your state automatically! However, there are a few cases where you should give it a hand with some casting:
export const useUserStore = defineStore('user', {
state: () => {
return {
// for initially empty lists
userList: [] as UserInfo[],
// for data that is not yet loaded
user: null as UserInfo | null,
}
},
})
interface UserInfo {
name: string
age: number
}If you prefer, you can define the state with an interface and type the return value of state():
interface State {
userList: UserInfo[]
user: UserInfo | null
}
export const useUserStore = defineStore('user', {
state: (): State => {
return {
userList: [],
user: null,
}
},
})
interface UserInfo {
name: string
age: number
}Accessing the state
By default, you can directly read from and write to the state by accessing it through the store instance:
const store = useStore()
store.count++Yes, this means no verbose wrappers like in Vuex, you can directly bind that to v-model:
<input v-model="store.count" type="number" />::: info
You cannot add a new state property if you don't define it in `state()`. It must contain the initial state. e.g.: we can't do store.secondCount = 2 if secondCount is not defined in state().
:::
Resetting the state
In Option Stores, you can _reset_ the state to its initial value by calling the $reset() method on the store:
const store = useStore()
store.$reset()Internally, this calls the state() function to create a new state object and replaces the current state with it.
In Setup Stores, you need to create your own $reset() method:
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
function $reset() {
count.value = 0
}
return { count, $reset }
})Usage with the Options API
<VueSchoolLink href="https://vueschool.io/lessons/access-pinia-state-in-the-options-api" title="Access Pinia State via the Options API" />
For the following examples, you can assume the following store was created:
// Example File Path:
// ./src/stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
})If you are not using the Composition API, and you are using computed, methods, ..., you can use the mapState() helper to map state properties as readonly computed properties:
import { mapState } from 'pinia'
import { useCounterStore } from '../stores/counter'
export default {
computed: {
// gives access to this.count inside the component
// same as reading from store.count
...mapState(useCounterStore, ['count'])
// same as above but registers it as this.myOwnName
...mapState(useCounterStore, {
myOwnName: 'count',
// you can also write a function that gets access to the store
double: store => store.count * 2,
// it can have access to `this` but it won't be typed correctly...
magicValue(store) {
return store.someGetter + this.count + this.double
},
}),
},
}Modifiable state
If you want to be able to write to these state properties (e.g. if you have a form), you can use mapWritableState() instead. Note you cannot pass a function like with mapState():
import { mapWritableState } from 'pinia'
import { useCounterStore } from '../stores/counter'
export default {
computed: {
// gives access to this.count inside the component and allows setting it
// this.count++
// same as reading from store.count
...mapWritableState(useCounterStore, ['count']),
// same as above but registers it as this.myOwnName
...mapWritableState(useCounterStore, {
myOwnName: 'count',
}),
},
}:::tip You don't need mapWritableState() for collections like arrays unless you are replacing the whole array with cartItems = [], mapState() still allows you to call methods on your collections. :::
Mutating the state
Apart from directly mutating the store with store.count++, you can also call the $patch method. It allows you to apply multiple changes at the same time with a partial state object:
store.$patch({
count: store.count + 1,
age: 120,
name: 'DIO',
})However, some mutations are really hard or costly to apply with this syntax: any collection modification (e.g. pushing, removing, splicing an element from an array) requires you to create a new collection. Because of this, the $patch method also accepts a function to group these kinds of mutations that are difficult to apply with a patch object:
store.$patch((state) => {
state.items.push({ name: 'shoes', quantity: 1 })
state.hasChanged = true
})The main difference here is that $patch() allows you to group multiple changes into one single entry in the devtools. Note that both direct changes to `state` and `$patch()` are tracked in devtools and can be time traveled.
Replacing the state
You cannot exactly replace the state of a store as that would break reactivity. You can however _patch it_:
// this doesn't actually replace `$state`
store.$state = { count: 24 }
// it internally calls `$patch()`:
store.$patch({ count: 24 })You can also set the initial state of your whole application by changing the state of the pinia instance. This is used during SSR for hydration.
pinia.state.value = {}Subscribing to the state
You can watch the state and its changes through the $subscribe() method of a store, similar to Vuex's subscribe method. The advantage of using $subscribe() over a regular watch() is that _subscriptions_ will trigger only once after _patches_ (e.g. when using the function version from above).
cartStore.$subscribe((mutation, state) => {
// import { MutationType } from 'pinia'
mutation.type // 'direct' | 'patch object' | 'patch function'
// same as cartStore.$id
mutation.storeId // 'cart'
// only available with mutation.type === 'patch object'
mutation.payload // patch object passed to cartStore.$patch()
// persist the whole state to the local storage whenever it changes
localStorage.setItem('cart', JSON.stringify(state))
})Flush timing
Under the hood, $subscribe() uses Vue's watch() function. You can pass the same options as you would with watch(). This is useful when you want to immediately trigger subscriptions after each state change:
```ts{4} cartStore.$subscribe((mutation, state) => { // persist the whole state to the local storage whenever it changes localStorage.setItem('cart', JSON.stringify(state)) }, { flush: 'sync' })
### Detaching subscriptions
By default, _state subscriptions_ are bound to the component where they are added (if the store is inside a component's `setup()`). Meaning, they will be automatically removed when the component is unmounted. If you also want to keep them after the component is unmounted, pass `{ detached: true }` as the second argument to _detach_ the _state subscription_ from the current component:
<script setup> const someStore = useSomeStore()
// this subscription will be kept even after the component is unmounted someStore.$subscribe(callback, { detached: true }) </script>
:::tip
You can _watch_ the whole state on the `pinia` instance with a single `watch()`:
watch( pinia.state, (state) => { // persist the whole state to the local storage whenever it changes localStorage.setItem('piniaState', JSON.stringify(state)) }, { deep: true } )
:::
Getting Started
Installation
<VueMasteryLogoLink for="pinia-cheat-sheet"> </VueMasteryLogoLink>
Install pinia with your favorite package manager:
::: code-group
```bash [npm] npm install pinia
yarn add pinia
pnpm add pinia
bun add pinia
:::
:::tip
If your app is using Vue <2.7, you also need to install the composition api: `@vue/composition-api`. If you are using Nuxt, you should follow [these instructions](/ssr/nuxt.md).
:::
If you are using the Vue CLI, you can instead give this **unofficial plugin** a try.
Create a pinia instance (the root store) and pass it to the app as a plugin:
import { createApp } from 'vue' import { createPinia } from 'pinia' import App from './App.vue'
const pinia = createPinia() const app = createApp(App)
app.use(pinia) app.mount('#app')
## What is a Store?
A Store (like Pinia) is an entity holding state and business logic that isn't bound to your Component tree. In other words, **it hosts global state**. It's a bit like a component that is always there and that everybody can read off and write to. It has **three concepts**, the [state](./core-concepts/state.md), [getters](./core-concepts/getters.md) and [actions](./core-concepts/actions.md) and it's safe to assume these concepts are the equivalent of `data`, `computed` and `methods` in components.
<RuleKitLink />
## When should I use a Store
A store should contain data that can be accessed throughout your application. This includes data that is used in many places, e.g. User information that is displayed in the navbar, as well as data that needs to be preserved through pages, e.g. a very complicated multi-step form.
On the other hand, you should avoid including in the store local data that could be hosted in a component instead, e.g. the visibility of an element local to a page.
Not all applications need access to a global state, but if yours need one, Pinia will make your life easier.
## When should I **not** use a Store
Sometimes we end up using a store for too many things. If you feel like your application is over using stores, you might want to re consider the purposes of your stores. Namely, if some of their logic should just be composables or if some of their state should be local to a component. This is covered in depth in the (Not) Overusing stores lesson of Mastering Pinia.
API 文档 / pinia / MutationType
Enumeration: MutationType %{#enumeration-mutationtype}%
pinia.MutationType
SubscriptionCallback 的可能类型
Enumeration Members %{#enumeration-members}%
direct %{#direct}%
• direct = "direct"
Direct mutation of the state:
store.name = 'new name'store.$state.name = 'new name'store.list.push('new item')
---
patchFunction %{#patchfunction}%
• patchFunction = "patch function"
通过 $patch 和一个函数更改 state:
store.$patch(state => state.name = 'newName')
---
patchObject %{#patchobject}%
• patchObject = "patch object"
通过 $patch 和一个对象更改 state:
store.$patch({ name: 'newName' })