State Management Patterns

Service-With-a-Signal Beats Most State Libraries

State Management Patterns

Most apps need a simple service that exposes signals — not a full Redux setup. Reach for libraries only when you have outgrown the simple thing.

4 min read Level 2/5 #angular#state#architecture
What you'll learn
  • Build a state service with a writable signal
  • Expose read-only signals via asReadonly()
  • Recognize when to step up to NgRx

Before reaching for a state library, try the simplest thing that works: an injectable service that owns a signal. Most features never need more than that.

A signal-backed service

import { Injectable, signal, computed } from '@angular/core';

export interface Item { id: string; name: string; price: number }

@Injectable({ providedIn: 'root' })
export class CartService {
  private _items = signal<Item[]>([]);

  // Public read-only view
  items = this._items.asReadonly();
  count = computed(() => this._items().length);
  total = computed(() => this._items().reduce((sum, i) => sum + i.price, 0));

  add(item: Item) {
    this._items.update(arr => [...arr, item]);
  }

  remove(id: string) {
    this._items.update(arr => arr.filter(i => i.id !== id));
  }

  clear() {
    this._items.set([]);
  }
}

asReadonly() hides the setter so components can only mutate state through methods — an encapsulation win.

Use it from a component

import { Component, inject } from '@angular/core';
import { CartService } from './cart.service';

@Component({
  selector: 'app-cart',
  standalone: true,
  template: `
    <h1>Cart ({{ cart.count() }})</h1>
    <p>Total: {{ cart.total() }}</p>
  `,
})
export class CartComponent {
  cart = inject(CartService);
}

When to step up

Move from a service to NgRx (or SignalStore) when:

  • State is shared by many unrelated features
  • You need disciplined logging or time-travel debugging
  • Side effects are stacking up and need orchestration
  • The team needs strict patterns to stay consistent

Until those problems hit, a handful of well-named services keeps the codebase small, fast, and testable.

Unit Testing Components →