Angular State Management in 2026 — NgRx, Signals, NGXS, Akita Compared (with Real Numbers)
We rewrote the same /campaigns feature six ways. Bundle 38 → 18 KB. State LOC 8,400 → 3,100. The library matters less than the kind of state you're storing.
TL;DR — We rebuilt Mattrx's Angular state layer over 8 months (Angular 16 → 19, ~22k LOC TS). State LOC 8,400 → 3,100 (-63%), state-related bundle 38 KB → 18 KB, /campaigns re-renders per filter keystroke 47 → 3, "where does this state live?" PR comments 14/week → 2.
The win wasn't picking the right library. It was picking the right library for each kind of state.
Full deep-dive (with all 6 code samples, bundle table, re-render benchmarks, decision tree, and the Mattrx production layout): https://prepstack.co.in/blog/angular-state-management-comparison-ngrx-signals-ngxs-akita-guide
The mental model first
Most teams treat state as one thing. There are four kinds, and each has a different right answer.
1. Local UI state — dropdown open, active tab, current input. Lives in one component. Right answer: signal() in the component.
2. Server cache — current list from the API. Lives in a service, cached, invalidated. Right answer: toSignal(http.get(...)) — or NgRx Entity if mutations are complex.
3. Shared feature state — selected rows, bulk-edit draft. Lives in a service. Right answer: signal in a feature service.
4. App-wide workflow state — multi-step transitions with audit log, retry, time-travel debugging. Right answer: NgRx (SignalStore for new code; classic if you need full Redux pattern with Effects).
Trying to use one library for all four — which everyone tried with NgRx around 2019 — is what generated the "NgRx is too much boilerplate" backlash. The library wasn't wrong; the scope it was applied to was.
The same feature, six ways
Mattrx's /campaigns page (list, debounced search, multi-select, optimistic archive, audit log) implemented in six libraries:
— Pure Signals: 0 KB, 70 LOC, 2 files. Mattrx default — covers 80% of state.
— NgRx SignalStore: ~6 KB, 75 LOC, 2 files. Where new NgRx code goes in 2026. Boilerplate gap with Signals is now 5 LOC, not 140.
— NgRx ComponentStore: ~5 KB. Feature-local heavy workflows.
— NgRx classic Store + Effects + Entity: ~25 KB, 210 LOC, 5 files. Full Redux pattern. Still right when you need airtight Action/Reducer separation.
— NGXS: ~14 KB, 140 LOC. Smaller-boilerplate alternative to classic NgRx. Not adopted at Mattrx — no concrete advantage over SignalStore in 2026.
— Akita: ~10 KB, 100 LOC, 4 files. Deprecated (maintenance mode since 2023).
The surprising re-render result
The big jump in render granularity — 47 re-renders per keystroke down to 3 — does not come from picking NgRx vs Signals. It comes from moving to Signals at all, regardless of library wrapping them.
NgRx classic with store.selectSignal() performs the same as pure Signals. BehaviorSubject + | async still pays the one-per-consumer cost.
If you are already on NgRx classic, you do not need to migrate libraries. You just need to switch your selectors from select() to selectSignal().
The 2026 rule of thumb
In one line:
Signals for state. RxJS for streams. NgRx SignalStore when the state is shared, workflow-heavy, and benefits from DevTools or time-travel.
The full deep-dive with all six code samples, the complete bundle table, re-render benchmarks, DevTools comparison, decision tree, and the Mattrx production layout (library per feature) is on PrepStack:
https://prepstack.co.in/blog/angular-state-management-comparison-ngrx-signals-ngxs-akita-guide
If this saved you a code-review argument, a 💖 or bookmark helps it reach more Angular devs.
