State Management (Service-with-Subject, NgRx & SignalStore)
As apps grow, shared data needs a single, predictable home — from a simple service to a full store like NgRx.
What you will learn
- Explain what “state” is and why a single source of truth helps
- Build a lightweight store with a service and a Subject/signal
- Understand NgRx’s store, actions, reducers and selectors conceptually
What is “state”?
State is just the data your app holds in memory right now: the logged-in user, the items in a cart, whether a panel is open. The problem in big apps is that many components need the same data, and if each keeps its own copy they drift out of sync — one shows 3 cart items, another shows 2.
The fix is a single source of truth: one place owns the data, everything reads from it, and changes flow through it. Angular gives you a ladder of options, from simple to powerful — pick the smallest one that fits.
| Approach | Good for | Effort |
|---|---|---|
| Service + BehaviorSubject/signal | Most small/medium apps | Low |
| NgRx SignalStore | Bigger apps, signal-based | Medium |
| NgRx Store (Redux-style) | Large enterprise apps, audited state | High |
The simplest store: a service with a signal
For most apps you do not need a library at all. A service that owns a signal (or a BehaviorSubject) is a perfectly good store: it holds the data, exposes a read-only view, and offers methods to change it.
// cart.service.ts
import { Injectable, signal, computed } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class CartService {
private items = signal<string[]>([]);
readonly all = this.items.asReadonly(); // read-only view
readonly count = computed(() => this.items().length);
add(item: string) {
this.items.update(list => [...list, item]); // new array
}
clear() {
this.items.set([]);
}
}Note: Output:
(No visible output by itself — this is the data layer.)
Every component injecting CartService shares the same signal, so they all agree on the cart. all exposes the data read-only (components cannot mutate it directly); count derives the number automatically; add/clear are the only ways to change it. That single-entry rule is what keeps state predictable.
When you outgrow it: NgRx
NgRx is the popular Redux-style state library for large Angular apps. Enterprises like it because state changes are explicit and traceable — you can see exactly what happened and replay it in DevTools. It has four core pieces:
- Store — the single object holding all app state.
- Actions — plain messages describing “something happened” (e.g.
[Cart] Add Item). - Reducers — pure functions that take the current state + an action and return the new state.
- Selectors — functions that read a slice of state for a component.
- Effects — handle side effects like HTTP calls, then dispatch a result action.
The flow is a strict one-way loop. You never change state directly — you dispatch an action, and a reducer produces the next state:
- A component dispatches an action (e.g. “add item to cart”).
- A reducer receives the action and returns a brand-new state object.
- The store updates and notifies anyone subscribed.
- A selector delivers the changed slice to the component, which re-renders.
- (For HTTP) an effect runs the API call and dispatches a “loaded” action when done.
// a tiny NgRx reducer (concept)
import { createReducer, on } from '@ngrx/store';
import { addItem } from './cart.actions';
const initialState = { items: [] as string[] };
export const cartReducer = createReducer(
initialState,
on(addItem, (state, { item }) => ({
...state,
items: [...state.items, item] // return NEW state, never mutate
}))
);Note: Output:
(No visible output by itself.)
When the addItem action is dispatched, this reducer makes a new state with the item appended. It never edits the old state in place — that immutability is what lets DevTools show a clean history and lets Angular detect the change cheaply.
SignalStore: the modern middle ground
NgRx SignalStore is a newer, lighter option that gives you a structured store built on signals, with far less boilerplate than classic NgRx. For many 2025-era apps it is the sweet spot between a hand-rolled service and full Redux-style NgRx.
Watch out: Do not reach for NgRx on day one. Most apps are well served by a service-with-signal store. Add a library only when shared state becomes genuinely hard to manage — over-engineering early just slows you down.
Tip: The one principle that matters across all of these: one source of truth, changed in one controlled way. Whether it is a signal in a service or a full NgRx store, never let components secretly mutate shared data.
Q. In NgRx, what is a reducer’s job?
✍️ Practice
- Build a signal-based
CartServicewithadd,removeand acountcomputed signal. - Show the cart count in a nav bar and the items on a cart page from the same service.
🏠 Homework
- Convert a piece of shared state (e.g. a theme: light/dark) into a service-with-signal store and read it from two components.