How to create a per-component store and use it in Angular components in this project. Use when: creating a new store, adding state to a component, wiring up httpResource or BehaviorSubject-based data, using InjectionToken factory providers, or injecting a store into child components.
This project uses a plain @Injectable() class as a store — no NgRx ComponentStore. Each feature area gets its own store class, provided at the component level so Angular creates a fresh instance per component tree. Use an InjectionToken only when you need to swap implementations (e.g. testing, multi-tenant); otherwise provide the class directly.
// my-feature.store.ts
import { inject, Injectable, signal, computed } from '@angular/core';
import { httpResource } from '@angular/common/http';
import { toSignal } from '@angular/core/rxjs-interop';
import { BehaviorSubject, switchMap } from 'rxjs';
import { MyService } from '@core/services';
@Injectable()
export class MyFeatureStore {
// ─── DI (use inject(), never constructor params) ───────────────────────────
private myService = inject(MyService);
// ─── Input signals (drive reactive requests) ───────────────────────────────
itemId = signal<number>(0);
// ─── Modern: httpResource (auto-fetches when signals change) ──────────────
data = httpResource<ItemModel>(() => {
const id = this.itemId();
return id ? { url: this.myService.getUrl() + `${id}` } : undefined;
});
// ─── RxJS style (BehaviorSubject + pipe + toSignal) ───────────────────────
reload$ = new BehaviorSubject<number>(0);
items$ = this.reload$.pipe(
switchMap(() => this.myService.list()),
// shareReplay(1) if multiple subscribers
);
items = toSignal(this.items$, { initialValue: [] });
// ─── Derived / computed state ──────────────────────────────────────────────
activeItems = computed(() => (this.items() || []).filter(i => i.active));
// ─── UI state ─────────────────────────────────────────────────────────────
// Note: httpResource already exposes data.isLoading() — no need for a separate isLoading signal.
selectedId = signal<number | null>(null);
// ─── Actions ──────────────────────────────────────────────────────────────
reload() {
this.reload$.next(this.reload$.value + 1);
}
select(id: number) {
this.selectedId.set(id);
}
}
The simplest way — provide the class directly. Angular creates one instance per component tree.
// my-feature-root.component.ts
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { map } from 'rxjs';
import { toSignal } from '@angular/core/rxjs-interop';
import { MyFeatureStore } from './my-feature.store';
@Component({
selector: 'app-my-feature',
templateUrl: './my-feature-root.component.html',
standalone: true,
providers: [MyFeatureStore], // ← one instance per component tree
})
export class MyFeatureRootComponent {
private store = inject(MyFeatureStore);
private route = inject(ActivatedRoute);
// Reactive route params → store input
private id = toSignal(this.route.params.pipe(map(p => +p['id'])));
constructor() {
effect(() => {
const id = this.id();
if (id) this.store.itemId.set(id);
});
}
}
Use an InjectionToken only when you need to swap the store for testing or multi-tenant scenarios.
// my-feature.models.ts
import { InjectionToken } from '@angular/core';
import { MyFeatureStore } from './my-feature.store';
export const MY_FEATURE_STORE = new InjectionToken<MyFeatureStore>('MY_FEATURE_STORE');
// useClass lets Angular instantiate the class in a proper injection context
// (inject() calls inside the store will work correctly).
// NEVER use useFactory: () => new MyFeatureStore() — that bypasses DI and inject() will throw NG0203.
export const MY_FEATURE_STORE_PROVIDER = {
provide: MY_FEATURE_STORE,
useClass: MyFeatureStore,
};
Then in the component: