Every Angular developer faces it eventually: your app grows, components need to share state, and suddenly you're asking — how should I manage this?
In 2026, you have three solid options: plain Services, Angular Signals, and NgRx. Each has a sweet spot. This article breaks them down with real examples so you can make the right call for your project.
Option 1 — Services (The Classic Approach)
Services with BehaviorSubject have been the go-to solution for years, and for good reason — they're simple, well-understood, and require zero extra dependencies.
@Injectable({ providedIn: 'root' })
export class CartService {
private _items = new BehaviorSubject<Item[]>([]);
items$ = this._items.asObservable();
addItem(item: Item) {
this._items.next([...this._items.value, item]);
}
}
They are best for:
Small to medium apps
Teams already comfortable with RxJS
State that lives in one or two places
But be carefull because:
No dev tools or traceability
Can get messy as the app scales
Easy to create inconsistencies across multiple services
Option 2 — Signals (The Modern Default)
Introduced in Angular 16 and now fully stable, Signals are Angular's built-in reactivity primitive. They eliminate most RxJS boilerplate for UI state and integrate natively with the template engine.
@Injectable({ providedIn: 'root' })
export class CartService {
private _items = signal<Item[]>([]);
items = this._items.asReadonly();
total = computed(() => this._items().reduce((sum, i) => sum + i.price, 0));
addItem(item: Item) {
this._items.update(current => [...current, item]);
}
}
Your template reacts automatically — no async pipe, no manual subscription:
@for (item of cartService.items(); track item.id) {
<p>{{ item.name }} — {{ item.price }}</p>
}
<strong>Total: {{ cartService.total() }}</strong>
They are best for:
New Angular projects (17+)
Local and shared UI state
Teams wanting to reduce RxJS complexity
But be carefull because:
Not ideal for complex async flows (RxJS still wins there)
Relatively new — fewer patterns established in large codebases
Option 3 — NgRx (The Enterprise Powerhouse)
NgRx implements the Redux pattern in Angular. Actions, reducers, selectors, effects — everything is explicit, traceable, and testable.
// Action
export const addItem = createAction('[Cart] Add Item', props<{ item: Item }>());
// Reducer
export const cartReducer = createReducer(
initialState,
on(addItem, (state, { item }) => ({
...state,
items: [...state.items, item]
}))
);
// Selector
export const selectCartItems = createSelector(
selectCartState,
state => state.items
);
// In component
items$ = this.store.select(selectCartItems);
Best for:
- Large enterprise apps
- Big teams needing shared conventions
- Complex async flows and side effects
- When you need full state history & debugging via Redux DevTools
Be carefull about:
- Significant boilerplate (NgRx Signals Store reduces this)
- Steep learning curve for newcomers
- Overkill for small/medium apps
Side-by-Side Comparison
| Services | Signals | NgRx | |
|---|---|---|---|
| Boilerplate | Low | Very Low | High |
| Learning Curve | Low | Low | High |
| Scalability | Medium | Medium-High | Very High |
| Dev Tools | Nope | Nope | Yes |
| Async Handling | RxJS | Limited | Effects |
| Best App Size | Small | Small–Medium | Large |
My Decision Framework
Is your app small or a prototype?
- Services or Signals
Is your team new to Angular?
- Signals (less to learn)
Do you have complex async flows (websockets, polling, chained HTTP)?
- NgRx Effects or RxJS in a Service
Is your team large with multiple feature modules?
- NgRx (with NgRx Signals Store to reduce boilerplate)
Can You Mix Them?
Absolutely — and many real-world apps do. A common pattern is:
Signals for local UI state (form state, toggles, UI flags)
Services for simple shared state
NgRx only for the most complex global state (auth, cart, notifications)
Don't feel pressured to pick one and apply it everywhere.
There's no single winner in Angular state management — the right tool depends on your app's complexity, your team's experience, and how much structure you need.
My personal default in 2026: start with Signals, reach for NgRx only when the complexity demands it.
Top comments (0)