Going DeeperPro· 45 min read

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.

ApproachGood forEffort
Service + BehaviorSubject/signalMost small/medium appsLow
NgRx SignalStoreBigger apps, signal-basedMedium
NgRx Store (Redux-style)Large enterprise apps, audited stateHigh

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.

A signal-based store inside a service
// 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:

  1. A component dispatches an action (e.g. “add item to cart”).
  2. A reducer receives the action and returns a brand-new state object.
  3. The store updates and notifies anyone subscribed.
  4. A selector delivers the changed slice to the component, which re-renders.
  5. (For HTTP) an effect runs the API call and dispatches a “loaded” action when done.
A reducer returns new state for an action
// 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?

Answer: A reducer is a pure function: given the current state and an action, it returns a new state object. Side effects like HTTP live in effects, not reducers.

✍️ Practice

  1. Build a signal-based CartService with add, remove and a count computed signal.
  2. Show the cart count in a nav bar and the items on a cart page from the same service.

🏠 Homework

  1. Convert a piece of shared state (e.g. a theme: light/dark) into a service-with-signal store and read it from two components.
Want to learn this with a mentor?

CodingClave runs guided, project-based training (28-day, 45-day & 6-month batches).

Explore Training →