Scoped Pinia Stores for Vue.js
Pinia Scope allows creating dynamically scoped versions of Pinia stores. A scope is a string that prefixes one or more Pinia Store ids separating them from their un-scoped version. After setting the scope of a component, its child components inherit that scope. This allows dynamic creation and disposal of the data layer and improved component re-use in a Vue app.
$ npm i pinia-scope
Attach pinia scope to the pinia instance in your main.js file.
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
// add import here
import { attachPiniaScope } from 'pinia-scope'
const app = createApp(App)
const pinia = createPinia()
// attaching here
attachPiniaScope(pinia)
app.use(pinia)
app.mount('#app')Pinia stores have a store.$id set when defining the store.
A scoped store has a scope string prefixed onto its store.$id:
// normal pinia code
export const useMyStore = (scope: string = '') => {
const myStore = defineStore(scope + 'my-store-id', () => { /* ... */
})
return myStore()
}
const myUnscopedStore = useMyStore()
myUnscopedStore.$id // 'my-store-id'
const myScopedStore = useMyStore('preview-scope-')
myScopedStore.$id // 'preview-scope-my-store-id'The above lets you create separate instances of the same store, but does not have the Abilities of:
🔥Pinia Scope 🔥:
- Apply scope to other stores it uses internally
- Handle component scope inheritance
- Handle cleaning up scoped stores after they are done being used
Stores created with pinia's defineStore() are wrapped in a store creator function.
Definition
// vehicle-store.ts
import { defineScopeableStore, type StoreCreatorContext } from 'pinia-scope'
// has the same signature as the pinia defineStore() method, but context is passed to the setup function
export const useVehicleStore = defineScopeableStore('vehicles', ({ scope }: StoreCreatorContext) => {
// use the same scope
const engineStore = useEngineStore(scope)
//...
})See: Pinia defineStore()
Usage in Component
<script setup lang="ts">
import { useVehicleStore } from 'vehicle-store.ts'
const vehicleStoreScoped = useVehicleStore('my-scope')
vehicleStoreUnScoped.$id // 'my-scope-vehicles'
const vehicleStoreUnScoped = useVehicleStore.unScoped()
vehicleStoreUnScoped.$id // 'vehicles'
// scoped by in this or parent component setComponentScope()
// or unScoped if never set
const vehicleStoreWithComponentScope = useVehicleStore()
</script>// vehicle-store.ts
import { type StoreCreatorContext, defineScopeableStore } from 'pinia-scope'
import { useTireStore } from 'tire-store.ts'
export const useVehicleStore = defineScopeableStore('vehicles', ({ scope }: StoreCreatorContext) => {
// create a TireStore using the same scope as this VehicleStore
// if this VehicleStore is unscoped, scope will be '' and the TireStore will also be unscoped
const tireStore = useTireStore(scope)
// creates a TireStore with no prefix (an un-scoped version of the TireStore)
// this would be the same as useTireStore() in a normal Pinia store
const tireStoreWithoutScope = useTireStore.unScoped()
// ❌ you should never use component scope within a store
const tireStoreComponentScoped = useTireStore()
// ...
})<script setup lang="ts">
import { setComponentScope } from 'pinia-scope'
import { useVehicleStore } from 'vehicle-store.ts'
// uses scope of a parent component if set
// if not set the default scope is '' (empty string) aka un-scoped
const parentScopedVehicleStore = useVehicleStore.injectedScope()
// set scope for this component and its children
setComponentScope('order-preview')
// uses 'order-preview' scope from above
const vehicleStore = useVehicleStore.injectedScope()
vehicleStore.$id // 'order-preview-vehicles'
</script>| Case | In Component | In Store | |
|---|---|---|---|
| Use component injected scope if available | useVehicleStore() |
✅ use by default | ❌ never use |
| UnScoped | useVehicleStore.unScoped() |
||
| Scoped | useVehicleStore(scope) |
âś… use by default |
Scope can also be set via the PiniaScopeProvider component.
Note:
Stores are only instantiated once when a component is mounted.
The value of a store variable (const vehicleStore = useStore(VehicleStore)) cannot be reactive and therefore cannot
change after mounting the component.
To conditionally change the scope of a component, you can mount/unmount components with different scope using v-if=""
and the PiniaScopeProvider component.
<script setup lang="ts">
import { PiniaScopeProvider } from 'pinia-scope'
import { defineProps } from 'vue'
const { useScopeA } = defineProps({
useScopeA: Boolean
})
</script>
<template>
<PiniaScopeProvider v-if="useScopeA" scope="scope-a">
<div>Scope A</div>
</PiniaScopeProvider>
<PiniaScopeProvider v-else scope="scope-b">
<div>Scope B</div>
</PiniaScopeProvider>
</template>After a scope is created, its options cannot be changed.
| option | default | description |
|---|---|---|
autoDispose |
true |
If true, when there are no longer any mounted components using a scope, store.$dispose() will be called on all stores in the scope. Note: store.$dispose() does not delete the state data and the data will be re-used if the scope is created again later. Use autoClearState to do that. |
autoClearState |
true |
If true, when there are no longer any mounted components using a scope, delete pinia.state.value[store.$id] will be called on all stores in the scope. |
When attaching pinia scope, the default options for a specific scope(s) instead of repeating them.
// in main.js
import { attachPiniaScope } from 'pinia-scope'
// ...
attachPiniaScope(pinia, {
scopeOptions: {
'my-scope': {
autoDispose: false,
autoClearState: false,
}
}
})Scope Options can be se when calling setComponentScope(). If an options argument is provided, This will override any default scope options.
import { setComponentScope } from 'pinia-scope'
const storeOptions = {
autoDispose: false,
autoClearState: false,
}
setComponentScope('my-scope', storeOptions)// by default a scope is prefixed onto a store id with the following function:
const generateScopedStoreId = (scope: string, id: string) => `${scope}-${id}`
// example:
const vehicleStore = useVehicleStore('my-scope')
vehicleStore.$id // 'my-scope-vehicles'// in main.js
import { attachPiniaScope } from 'pinia-scope'
// ...
attachPiniaScope(pinia, {
// scoped store id generation can be customized
scopeNameGenerator: (scope: string, id: string) => `[${scope}]~~[${id}]`
})
// example
const vehicleStore = useVehicleStore('my-scope')
vehicleStore.$id // '[my-scope]~~[vehicles]'If you want to disable component scope auto injections and write code more manually you can.
// main.js
import { attachPiniaScope } from 'pinia-scope'
attachPiniaScope(pinia, {
autoInjectScope: false,
})
// in a component/store
import { useVehicleStore } from 'vehicle-store.ts'
// always behaves the same as useVehicleStore.unScoped()
const vehicleStore = useVehicleStore()
// gets scope from component if set by setComponentScope()
// always behaves the same as useVehicleStore() when autoInjectScope = true
const vehicleStoreUnscoped = useVehicleStore.componentScoped()
// stats the same
const vehicleStoreScoped = useVehicleStore('my-scope')The scope can be used to determine plugin options by passing a function as the store options argument.
// main.js
import { defineScopeableStore } from 'pinia-scope'
export const useVehicleStore = defineScopeableStore('vehicles', ({ scope }: StoreCreatorContext) => {
// ...
}, (scope: string) => {
return {
somePluginOption: scope === 'foo'
}
})All API methods (excluding attachPiniaScope()) will work inside a vue component or a context where getActivePinia() will return a result.
Returns a scoped store.
import { attachPiniaScope } from 'pinia-scope'
attachPiniaScope(pinia, {
// see above for options details
autoInjectScope: false,
scopeDefaults: { ... },
scopeNameGenerator: () => '',
})Returns the current scope or an empty string if no scope is set. Useful when debugging component scopes.
import { getComponentScope } from 'pinia-scope'
console.log(getComponentScope())Equivalent behavior to autoDispose = true and autoClearState = false scope options.
If you want to store.$dispose() a scope's stores manually and never clear its state,
set autoDispose = false and call disposeOfPiniaScope().
import { disposeOfPiniaScope } from 'pinia-scope'
disposeOfPiniaScope('my-scope')Equivalent to autoDispose = true and autoClearState = true scope options.
If you want to store.$dispose() a scope's stores manually and clear its state,
set autoDispose = false and autoClearState = false call disposeAndClearStateOfPiniaScope().
import { disposeAndClearStateOfPiniaScope } from 'pinia-scope'
disposeAndClearStateOfPiniaScope('my-scope')Inspects a store instance's Unscoped store.$id or current scope.
import { getStoreInfo, getStoreUnscopedId, getStoreScope } from 'pinia-scope'
const store = useMyStore()
const { unscopedId , scope } = getStoreInfo(store)
const unscopedId2 = getStoreUnscopedId(store)
const scope2 = getStoreScope(store)Loops over each store currently used by a given scope.
import { eachStoreOfPiniaScope } from 'pinia-scope'
import { type Store } from 'vue'
eachStoreOfPiniaScope('my-scope', (store: Store) => {
console.log(store.$state)
})To use pinia scope functions outside a component, you can manually set the active pinia instance. The same way you would for normal pinia stores.
import { useStore } from 'pinia-scope'
import { VehicleStore } from 'vehicle-store.ts'
import { createPinia, setActivePinia } from 'pinia'
import { attachPiniaScope } from './pinia-scope'
const pinia = createPinia()
setActivePinia(pinia)
attachPiniaScope(pinia)
const vehicleStore = useStore(VehicleStore)$ pnpm install
$ pnpm run build
$ pnpm run test
$ pnpm run test:mutation
Partly Inspired by: https://github.com/ccqgithub/pinia-di
- update
package.jsonfile version (example:1.0.99) - manually create a github release with a tag matching the
package.jsonversion prefixed withv(example:v1.0.99) - npm should be updated automatically