S

super-dev — full-stack

@super-dev.app · Full-stack · ★ open to work
12.8k subscribers·47 videos·Active since 2017

Full-stack .NET / Angular dev with a DevOps streak on Azure Cloud. Also fluent in Flutter + Firebase. I build products, ship them, and keep them healthy.

Back to articles
ANGULAR
ANGULAR

App state with NgRx SignalStore

Article 4 of 4 — Angular 21 in practice
Zoneless, signals, resource(), @defer, SignalStore — the modern front end, for real.

Not all of an app's state belongs in a signal() buried at the bottom of a component. As soon as a piece of state is shared, derived and mutated from several places, you want a clear boundary: read-only selectors, methods to evolve it. NgRx SignalStore ( @ngrx/signals ) gives you exactly that, without the action/reducer boilerplate of classic NgRx.

Anatomy of a signalStore

A store is built from chained features . withState declares the initial state, withComputed the derived values, withMethods the operations. Each state field automatically becomes a signal exposed on the instance.

TypeScript
1import { signalStore, withComputed, withMethods, withState } from '@ngrx/signals';
2import { computed } from '@angular/core';
3
4export const CartStore = signalStore(
5 { providedIn: 'root' },
6 withState<{ items: CartItem[] }>({ items: [] }),
7 withComputed(({ items }) => ({
8 total: computed(() => items().reduce((sum, item) => sum + item.price * item.quantity, 0)),
9 count: computed(() => items().length),
10 })),
11 withMethods((store) => ({
12 add(item: CartItem): void {
13 patchState(store, { items: [...store.items(), item] });
14 },
15 clear(): void {
16 patchState(store, { items: [] });
17 },
18 })),
19);

State is never mutated directly: you go through patchState , which applies an immutable update and notifies the affected signals.

Selectors that are signals

store.total and store.count aren't functions you call inside the service: they are computed s, so full-fledged signals. In a component you read them like any other signal, and zoneless change detection only re-renders what depends on them.

TypeScript
1export class CartBadge {
2 private readonly store = inject(CartStore);
3
4 protected readonly count = this.store.count;
5 protected readonly total = this.store.total;
6
7 protected checkout(): void {
8 this.store.clear();
9 }
10}

Composing with async calls

withMethods can integrate rxMethod (from @ngrx/signals/rxjs-interop ) to wire an RxJS stream, or simply async / await for a fetch . You keep orchestration logic inside the store, and the component stays a view. That's also where you place a loading state for a stale-while-revalidate pattern.

Store or plain signal?

Not everything needs a store. State local to a component — an active tab, an open menu — stays a private signal() : a store would add pointless indirection. The SignalStore earns its place when state is:

  • shared across several components or routes;
  • derived by several computed s you want to centralize;
  • mutated by operations you want to test in isolation.

The practical rule: start with local signals, extract a store the day you copy the same state into a second component. The docs cover every feature in the SignalStore guide .

The SignalStore isn't yesterday's "actions everywhere" NgRx. It's a façade of signals: read-only out, methods in, zero reducers. You keep the discipline of a store without paying its ceremony.
super-dev — portfolio.app
// More in ANGULAR
A DOOM-style engine in the browser, with three backends
11 min • 1.4k reads
Take an Angular app zoneless + signals
8 min • 4.1k reads
Loading data with resource() and httpResource()
7 min • 3.6k reads