npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@iappx/pinia-di

v1.0.0

Published

Class-based Pinia stores with constructor dependency injection powered by tsyringe.

Downloads

123

Readme

Pinia DI

npm version license

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-metadata

vue (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 $state and 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 Ref inside it stays a Ref. A value derived from a dependency (this.query = api.query()) is an ordinary field and therefore state.
  • this is 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