PraxisJS

Store

@praxisjs/store — class-based singleton stores with @Store and @UseStore. Define reactive state with @State, inject anywhere with @UseStore.

Store

Class-based singleton state management. Define a store once with @Store, inject it into any component with @UseStore. All components reading the same store stay in sync automatically.

npm install @praxisjs/store
pnpm add @praxisjs/store
yarn add @praxisjs/store
bun add @praxisjs/store

Defining a store with @Store()

Extends ReactiveStore to use @State, @Computed, and @DeepState on store fields. The class is instantiated once on first use and the same instance is shared everywhere.

import { Store, ReactiveStore } from '@praxisjs/store'
import { State, Computed } from '@praxisjs/decorators'

@Store()
class CartStore extends ReactiveStore {
  @State() items: Product[] = []
  @State() discount = 0

  @Computed()
  get total() {
    return this.items.reduce((sum, i) => sum + i.price, 0) * (1 - this.discount)
  }

  addItem(product: Product) {
    this.items = [...this.items, product]
  }

  removeItem(id: string) {
    this.items = this.items.filter(i => i.id !== id)
  }

  applyDiscount(rate: number) {
    this.discount = rate
  }
}

Why extend ReactiveStore?

ReactiveStore provides the internal state tracking that @State and @DeepState require on non-component classes. It also lets TypeScript validate that reactive decorators are used only on classes designed to hold reactive state.

Storybook
Live demo — @Store / @UseStore

Injecting a store with @UseStore(StoreClass)

Injects the singleton store instance into a component field. The store is created on first access — subsequent injections return the same instance.

import { UseStore } from '@praxisjs/store'

@Component()
class CartButton extends StatefulComponent {
  @UseStore(CartStore) cart!: CartStore

  render() {
    return (
      <button onClick={() => this.cart.addItem(product)}>
        Add to cart ({() => this.cart.items.length})
      </button>
    )
  }
}

Any component that reads a reactive property from the store updates automatically when that property changes — no subscriptions, no context providers.


Sharing state across components

Multiple components can inject the same store. They all share the same instance and react to the same changes:

@Component()
class CartSummary extends StatefulComponent {
  @UseStore(CartStore) cart!: CartStore

  render() {
    return (
      <div>
        <p>{() => this.cart.items.length} items</p>
        <p>Total: ${() => this.cart.total.toFixed(2)}</p>
      </div>
    )
  }
}

@Component()
class CheckoutPage extends StatefulComponent {
  @UseStore(CartStore) cart!: CartStore

  render() {
    return (
      <div>
        {() => this.cart.items.map(item => <CartItem item={item} />)}
        <button onClick={() => this.cart.applyDiscount(0.1)}>Apply 10% off</button>
      </div>
    )
  }
}

When any component calls cart.addItem(), both CartSummary and CheckoutPage update automatically — because they both read reactive properties from the shared store instance.


Auth store example

A common pattern — a store that holds authentication state and exposes derived values:

@Store()
class AuthStore extends ReactiveStore {
  @State() user: User | null = null

  @Computed()
  get isLoggedIn() { return this.user !== null }

  login(user: User) { this.user = user }
  logout() { this.user = null }
}

// In any component:
@UseStore(AuthStore) auth!: AuthStore

render() {
  return () => this.auth.isLoggedIn
    ? <UserMenu user={this.auth.user!} />
    : <LoginButton />
}

On this page