@iappx/pinia-di
v1.0.0
Published
Class-based Pinia stores with constructor dependency injection powered by tsyringe.
Downloads
123
Maintainers
Readme
Pinia DI
Class-based Pinia stores with constructor dependency injection powered by tsyringe.
Write a store as a class, declare its services as constructor parameters, and resolve it from the container like any other service. Fields become state, getters become computed values, methods become actions.
import { InjectableStore, StoreBase } from '@iappx/pinia-di'
import { inject } from 'tsyringe'
import { UserApi, type User } from './UserApi'
@InjectableStore
export class UserStore extends StoreBase {
public user: User | null = null
public loading = false
constructor(@inject(UserApi) private readonly api: UserApi) {
super()
}
public get isSignedIn(): boolean {
return this.user !== null
}
public async load(): Promise<void> {
this.loading = true
try {
this.user = await this.api.currentUser()
} finally {
this.loading = false
}
}
}const users = container.resolve(UserStore)
await users.load()Installation
npm install @iappx/pinia-di pinia tsyringe reflect-metadatavue (3.3+), pinia (2.1+ or 3), tsyringe and reflect-metadata are peer dependencies. Import
the polyfill once, before anything that touches tsyringe, and install Pinia as usual:
import 'reflect-metadata'
app.use(createPinia())TypeScript configuration
{
"compilerOptions": {
"experimentalDecorators": true
}
}emitDecoratorMetadata is optional. esbuild (and therefore Vite) does not emit it, so annotate
every constructor parameter with @inject(Token); that works with or without metadata.
Keep class names in production builds
A store's Pinia id defaults to its class name. A minifier that renames classes makes every store
collide, and @InjectableStore throws on the second one. Either keep names in the build
(keepNames in esbuild / Rolldown) or give each store an explicit id:
@InjectableStore({ id: 'user' })
export class UserStore extends StoreBase {}How a class becomes a store
- Fields are state. Every field the instance holds is part of the store's
$stateand is reactive, including a field first assigned in an action long after construction. - Getters are computed values. A getter with a setter is a writable computed; assigning to a getter without one throws.
- Methods are actions. Overriding works as in any class: the most derived method wins, and
super.load()reaches the parent. - Dependencies are not state. Whatever the container passed to the constructor stays a plain,
non-reactive field that only the store's own code can read: a service is the very instance the
container holds, and a
Refinside it stays aRef. A value derived from a dependency (this.query = api.query()) is an ordinary field and therefore state. thisis the store. Field initializers, the constructor, getters and actions all see the same live store, so a closure created in the constructor keeps working.setup()runs once, right after the store is built. Subscribe to events there, not in a getter.
When a store is built
Decorating a class only registers it. The store — and its constructor — runs when it is first resolved from the container, once per Pinia instance. A store can therefore depend on services that are registered later, at application start-up.
A failed construction leaves nothing behind: the next resolution builds the store again.
Stores depending on stores
Inject a store like any other dependency:
@InjectableStore
export class CartStore extends StoreBase {
constructor(@inject(UserStore) private readonly users: UserStore) {
super()
}
}Two stores may depend on each other through tokens resolved lazily (delay() or a factory).
While one of them is being built the other sees it unfinished, so do not call into the partner
from a constructor.
API
| Export | Description |
| --- | --- |
| @InjectableStore / @InjectableStore({ id }) | Registers the class as a singleton Pinia store in tsyringe's global container. |
| StoreBase | The base class every store extends. Declares the setup() hook. |
| PiniaDiError | Thrown on invalid declarations: a store not extending StoreBase, a duplicate id. |
Choosing the container
Stores are registered in tsyringe's global container, and their dependencies are resolved from
the container the store is resolved from — a child container works, too. Pinia keeps one instance
per store id, so the container that resolves a store first is the one its dependencies come from.
License
MIT
