Skip to content

Store

A store is a plain class whose fields are made reactive with @reactive. It holds state — no render(), no DOM. Any component, effect, or framework integration that reads a store field tracks it as a signal dependency and re-runs when it changes.

Create a shared store

import {
function reactive<This extends object, Value>(source?: (self: This) => Signal<Value>): (_target: unknown, context: ClassFieldDecoratorContext<This, Value>) => (this: This, initialValue: Value) => Value

A decorator that makes a class field reactive by automatically wrapping its value in a signal.

The field behaves like a normal property (get/set) but reactivity is tracked under the hood. Any reads will subscribe to the signal and any writes will trigger updates.

@example

class Counter {
\@reactive() count: number = 0;
}
const counter = new Counter();
counter.count++; // Triggers reactivity
console.log(counter.count); // Subscribes to changes

@remarks

Equivalent to manually creating a private signal and getter/setter:

class Counter {
#count = signal(0);
get count() { return this.#count(); }
set count(value) { this.#count(value); }
}

reactive
,
function computed<T>(getter: (previousValue?: T) => T): () => T

Creates a lazily-evaluated computed value.

The getter is only called when the computed value is read and one of its dependencies has changed since the last evaluation. If nothing has changed the cached value is returned without re-running getter.

Computed values are read-only; they cannot be set directly.

@paramgetter - Pure function deriving a value from other reactive sources. Receives the previous value as an optional optimisation hint.

@example

const a = signal(1);
const b = signal(2);
const sum = computed(() => a() + b());
sum(); // → 3
a(10);
sum(); // → 12 (re-evaluated lazily)

computed
} from "elements-kit/signals";
export class
class CartStore
CartStore
{
@
reactive<object, unknown>(source?: ((self: object) => Signal<unknown>) | undefined): (_target: unknown, context: ClassFieldDecoratorContext<object, unknown>) => (this: object, initialValue: unknown) => unknown

A decorator that makes a class field reactive by automatically wrapping its value in a signal.

The field behaves like a normal property (get/set) but reactivity is tracked under the hood. Any reads will subscribe to the signal and any writes will trigger updates.

@example

class Counter {
\@reactive() count: number = 0;
}
const counter = new Counter();
counter.count++; // Triggers reactivity
console.log(counter.count); // Subscribes to changes

@remarks

Equivalent to manually creating a private signal and getter/setter:

class Counter {
#count = signal(0);
get count() { return this.#count(); }
set count(value) { this.#count(value); }
}

reactive
()
CartStore.items: {
name: string;
price: number;
}[]
items
: {
name: string
name
: string;
price: number
price
: number }[] = [];
@
reactive<object, unknown>(source?: ((self: object) => Signal<unknown>) | undefined): (_target: unknown, context: ClassFieldDecoratorContext<object, unknown>) => (this: object, initialValue: unknown) => unknown

A decorator that makes a class field reactive by automatically wrapping its value in a signal.

The field behaves like a normal property (get/set) but reactivity is tracked under the hood. Any reads will subscribe to the signal and any writes will trigger updates.

@example

class Counter {
\@reactive() count: number = 0;
}
const counter = new Counter();
counter.count++; // Triggers reactivity
console.log(counter.count); // Subscribes to changes

@remarks

Equivalent to manually creating a private signal and getter/setter:

class Counter {
#count = signal(0);
get count() { return this.#count(); }
set count(value) { this.#count(value); }
}

reactive
()
CartStore.discount: number
discount
= 0;
CartStore.subtotal: () => number
subtotal
=
computed<number>(getter: (previousValue?: number | undefined) => number): () => number

Creates a lazily-evaluated computed value.

The getter is only called when the computed value is read and one of its dependencies has changed since the last evaluation. If nothing has changed the cached value is returned without re-running getter.

Computed values are read-only; they cannot be set directly.

@paramgetter - Pure function deriving a value from other reactive sources. Receives the previous value as an optional optimisation hint.

@example

const a = signal(1);
const b = signal(2);
const sum = computed(() => a() + b());
sum(); // → 3
a(10);
sum(); // → 12 (re-evaluated lazily)

computed
(() =>
this.
CartStore.items: {
name: string;
price: number;
}[]
items
.
Array<{ name: string; price: number; }>.reduce<number>(callbackfn: (previousValue: number, currentValue: {
name: string;
price: number;
}, currentIndex: number, array: {
name: string;
price: number;
}[]) => number, initialValue: number): number (+2 overloads)

Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.

@paramcallbackfn A function that accepts up to four arguments. The reduce method calls the callbackfn function one time for each element in the array.

@paraminitialValue If initialValue is specified, it is used as the initial value to start the accumulation. The first call to the callbackfn function provides this value as an argument instead of an array value.

reduce
((
sum: number
sum
,
i: {
name: string;
price: number;
}
i
) =>
sum: number
sum
+
i: {
name: string;
price: number;
}
i
.
price: number
price
, 0),
);
CartStore.total: () => number
total
=
computed<number>(getter: (previousValue?: number | undefined) => number): () => number

Creates a lazily-evaluated computed value.

The getter is only called when the computed value is read and one of its dependencies has changed since the last evaluation. If nothing has changed the cached value is returned without re-running getter.

Computed values are read-only; they cannot be set directly.

@paramgetter - Pure function deriving a value from other reactive sources. Receives the previous value as an optional optimisation hint.

@example

const a = signal(1);
const b = signal(2);
const sum = computed(() => a() + b());
sum(); // → 3
a(10);
sum(); // → 12 (re-evaluated lazily)

computed
(() => this.
CartStore.subtotal: () => number
subtotal
() * (1 - this.
CartStore.discount: number
discount
));
CartStore.add(item: {
name: string;
price: number;
}): void
add
(
item: {
name: string;
price: number;
}
item
: {
name: string
name
: string;
price: number
price
: number }) {
this.
CartStore.items: {
name: string;
price: number;
}[]
items
= [...this.
CartStore.items: {
name: string;
price: number;
}[]
items
,
item: {
name: string;
price: number;
}
item
];
}
CartStore.remove(name: string): void
remove
(
name: string
name
: string) {
this.
CartStore.items: {
name: string;
price: number;
}[]
items
= this.
CartStore.items: {
name: string;
price: number;
}[]
items
.
Array<{ name: string; price: number; }>.filter(predicate: (value: {
name: string;
price: number;
}, index: number, array: {
name: string;
price: number;
}[]) => unknown, thisArg?: any): {
name: string;
price: number;
}[] (+1 overload)

Returns the elements of an array that meet the condition specified in a callback function.

@parampredicate A function that accepts up to three arguments. The filter method calls the predicate function one time for each element in the array.

@paramthisArg An object to which the this keyword can refer in the predicate function. If thisArg is omitted, undefined is used as the this value.

filter
((
i: {
name: string;
price: number;
}
i
) =>
i: {
name: string;
price: number;
}
i
.
name: string
name
!==
name: string
name
);
}
}
export const
const cart: CartStore
cart
= new
constructor CartStore(): CartStore
CartStore
();

Stores are framework-agnostic — the same instance drives a custom element, a React component, and a plain effect, all in sync.


Reading from a store

@reactive() installs a get / set accessor on the class field. Reading the field calls the signal’s read function — so anywhere you pass () => store.field, you create a live subscription.

import { effect, computed } from "elements-kit/signals";
import { cart } from "./cart-store";
// Effect — reruns when cart.items or cart.discount changes
effect(() => {
console.log(cart.items.length, "items, total:", cart.total());
});
// In an element — live text binding
<p>Total: {() => cart.total()}</p>
// Compose into another computed
const isEmpty = computed(() => cart.items.length === 0);
// In React (via useSignal)
const items = useSignal(() => cart.items);
const total = useSignal(cart.total); // Computed<T> directly

Singleton vs. instance

Singleton — one shared instance for the whole app or a module:

cart-store.ts
export const cart = new CartStore();
// anywhere.ts
import { cart } from "./cart-store";
cart.add({ name: "Widget", price: 9.99 });

Instance — a new store per component or subtree:

class TodoComponent {
store = new TodoStore(); // private, scoped to this component
render() {
return <ul>{() => this.store.todos.map(...)}</ul>;
}
}

Store with actions

Group mutations as methods. Methods are plain functions — they write to @reactive fields and the reactivity system handles the rest.

import {
function reactive<This extends object, Value>(source?: (self: This) => Signal<Value>): (_target: unknown, context: ClassFieldDecoratorContext<This, Value>) => (this: This, initialValue: Value) => Value

A decorator that makes a class field reactive by automatically wrapping its value in a signal.

The field behaves like a normal property (get/set) but reactivity is tracked under the hood. Any reads will subscribe to the signal and any writes will trigger updates.

@example

class Counter {
\@reactive() count: number = 0;
}
const counter = new Counter();
counter.count++; // Triggers reactivity
console.log(counter.count); // Subscribes to changes

@remarks

Equivalent to manually creating a private signal and getter/setter:

class Counter {
#count = signal(0);
get count() { return this.#count(); }
set count(value) { this.#count(value); }
}

reactive
,
function computed<T>(getter: (previousValue?: T) => T): () => T

Creates a lazily-evaluated computed value.

The getter is only called when the computed value is read and one of its dependencies has changed since the last evaluation. If nothing has changed the cached value is returned without re-running getter.

Computed values are read-only; they cannot be set directly.

@paramgetter - Pure function deriving a value from other reactive sources. Receives the previous value as an optional optimisation hint.

@example

const a = signal(1);
const b = signal(2);
const sum = computed(() => a() + b());
sum(); // → 3
a(10);
sum(); // → 12 (re-evaluated lazily)

computed
} from "elements-kit/signals";
export class
class AuthStore
AuthStore
{
@
reactive<object, unknown>(source?: ((self: object) => Signal<unknown>) | undefined): (_target: unknown, context: ClassFieldDecoratorContext<object, unknown>) => (this: object, initialValue: unknown) => unknown

A decorator that makes a class field reactive by automatically wrapping its value in a signal.

The field behaves like a normal property (get/set) but reactivity is tracked under the hood. Any reads will subscribe to the signal and any writes will trigger updates.

@example

class Counter {
\@reactive() count: number = 0;
}
const counter = new Counter();
counter.count++; // Triggers reactivity
console.log(counter.count); // Subscribes to changes

@remarks

Equivalent to manually creating a private signal and getter/setter:

class Counter {
#count = signal(0);
get count() { return this.#count(); }
set count(value) { this.#count(value); }
}

reactive
()
AuthStore.user: {
name: string;
role: string;
} | null
user
: {
name: string
name
: string;
role: string
role
: string } | null = null;
@
reactive<object, unknown>(source?: ((self: object) => Signal<unknown>) | undefined): (_target: unknown, context: ClassFieldDecoratorContext<object, unknown>) => (this: object, initialValue: unknown) => unknown

A decorator that makes a class field reactive by automatically wrapping its value in a signal.

The field behaves like a normal property (get/set) but reactivity is tracked under the hood. Any reads will subscribe to the signal and any writes will trigger updates.

@example

class Counter {
\@reactive() count: number = 0;
}
const counter = new Counter();
counter.count++; // Triggers reactivity
console.log(counter.count); // Subscribes to changes

@remarks

Equivalent to manually creating a private signal and getter/setter:

class Counter {
#count = signal(0);
get count() { return this.#count(); }
set count(value) { this.#count(value); }
}

reactive
()
AuthStore.loading: boolean
loading
= false;
AuthStore.isAdmin: () => boolean
isAdmin
=
computed<boolean>(getter: (previousValue?: boolean | undefined) => boolean): () => boolean

Creates a lazily-evaluated computed value.

The getter is only called when the computed value is read and one of its dependencies has changed since the last evaluation. If nothing has changed the cached value is returned without re-running getter.

Computed values are read-only; they cannot be set directly.

@paramgetter - Pure function deriving a value from other reactive sources. Receives the previous value as an optional optimisation hint.

@example

const a = signal(1);
const b = signal(2);
const sum = computed(() => a() + b());
sum(); // → 3
a(10);
sum(); // → 12 (re-evaluated lazily)

computed
(() => this.
AuthStore.user: {
name: string;
role: string;
} | null
user
?.
role: string | undefined
role
=== "admin");
async
AuthStore.login(email: string, password: string): Promise<void>
login
(
email: string
email
: string,
password: string
password
: string) {
this.
AuthStore.loading: boolean
loading
= true;
try {
this.
AuthStore.user: {
name: string;
role: string;
} | null
user
= await
any
api
.
any
login
(
email: string
email
,
password: string
password
);
} finally {
this.
AuthStore.loading: boolean
loading
= false;
}
}
AuthStore.logout(): void
logout
() {
this.
AuthStore.user: {
name: string;
role: string;
} | null
user
= null;
}
}
export const
const auth: AuthStore
auth
= new
constructor AuthStore(): AuthStore
AuthStore
();

Composing stores

Stores can read from each other — just access their fields inside a computed:

export class UIStore {
@reactive() sidebarOpen = true;
// Derived from another store
pageTitle = computed(() =>
auth.user ? `Welcome, ${auth.user.name}` : "Sign in",
);
}
export const ui = new UIStore();

See also