Tiny helper for wiring Angular Signal-based stores with a read-only interface and a minimal API.
npm install ngx-simple-signal-storesetState, patchState, and resetStore with merge semantics for primitives, objects, and arrays.
dequal) to avoid unnecessary Signal emissions.
bash
npm install ngx-simple-signal-store
`
Quick start (global store)
Create (or let default) a token, provide the store, then inject and use it:
`ts
// app.config.ts
import { ApplicationConfig, inject } from '@angular/core';
import { createInjectionToken, provideStore } from 'ngx-simple-signal-store';
export interface DemoState {
theAnswerToLife: number;
}
const initialDemoState: DemoState = {
theAnswerToLife: 42,
};
// Pass a name (useful in dev tools) or omit to use the default label
export const demoStateToken = createInjectionToken('demoState');
export const appConfig: ApplicationConfig = {
providers: [
provideStore(initialDemoState, demoStateToken, {
// Optional: override equality comparer for all signals in this store
equal: (a, b) => a === b,
}),
],
};
`
`ts
// demo.component.ts
import { Component, inject } from '@angular/core';
import { demoStateToken } from './app.config';
@Component({
selector: 'app-demo',
template: The answer is {{ demoState.state.theAnswerToLife() }},
})
export class DemoComponent {
readonly demoState = inject(demoStateToken);
bump(): void {
this.demoState.patchState('theAnswerToLife', (value) => value + 1);
}
// Derived read-only signal
readonly doubled = this.demoState.select((s) => s.theAnswerToLife() * 2);
// Synchronous read (non-reactive)
logNow(): void {
console.log(this.demoState.getValue('theAnswerToLife'));
}
}
`
Component-scoped store
Provide a store only for a component subtree:
`ts
import { Component, inject } from '@angular/core';
import { createInjectionToken, provideStore } from 'ngx-simple-signal-store';
interface CounterState {
count: number;
}
const initialCounterState: CounterState = { count: 0 };
const counterStateToken = createInjectionToken('counterState');
@Component({
selector: 'app-counter',
template: Count: {{ counterStore.state.count() }},
providers: [provideStore(initialCounterState, counterStateToken)],
})
export class CounterComponent {
readonly counterStore = inject(counterStateToken);
increment(): void {
this.counterStore.patchState('count', (current) => current + 1);
}
}
`
API
Store shape
- state: { [K in keyof T]: Signal — read-only Signals for each state key; read with store.state.key().
getValue(key): T[K]
- Synchronously read the current value of one state key.
setState(key, value): void
- Replace a single state key with an exact value.
- Example: store.setState('count', 10);
patchState(key, value | partial | callback): void
- Primitives: overwrite (store.patchState('flag', true);).
- Arrays: append (store.patchState('items', ['new']);).
- Objects: shallow merge (store.patchState('user', { name: 'Neo' });).
- Callback: receives a cloned snapshot and returns the next value (store.patchState('count', (c) => c + 1);).
resetStore(): void
- Restore the initial state for every key.
select(project, equal?): Signal
- Create a derived Signal from the read-only state; optionally pass a comparer (defaults to dequal).
Utilities
- createInjectionToken — creates a typed token; name defaults to NgxSimpleSignalStore.
- provideStore — provider factory with optional equality comparer for Signal emissions.
Notes on equality
- The default comparer is dequal. Override per store via options.equal or per derived signal via the select` second argument.
| ngx-simple-signal-store | Angular |
|---|---|
|
- |
Newer versions follow Angular’s versioning format. |
|
3.x |
>= 20 |
|
2.x |
19.x.x |
|
1.x |
18.x.x |