Dec 12, 2025

Angular Signals Part 3: State Management, Stores & Reactive Forms

Tags

After seeing signals shine in components, the next logical step was tackling app-wide state. I remember wrestling with BehaviorSubject chains, manual subscriptions, and memory leak worries across services. Signals offered a cleaner path: global state without RxJS boilerplate. In this part, we'll build a shopping cart store and sync it with reactive forms using effect().​


angular-signal-state-management-stores-reactive-forms

Replacing BehaviorSubject with Signals in Services


The classic BehaviorSubject pattern in services looks like this:


// OLD: BehaviorSubject approach
private cartItems = new BehaviorSubject<Item[]>([]);
cartItems$ = this.cartItems.asObservable();
addItem(item: Item) { this.cartItems.next([...this.cartItems.value, item]); }



Signals make it simpler:



// NEW: Signal approach
private _items = signal<Item[]>([]);
readonly items = this._items.asReadonly();

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


No subscriptions needed! Components read this.cartService.items() directly in templates.


Building a Shopping Cart Store (Practical Example)


Here's my complete cart service using the "store pattern" with signals:


// cart.store.ts
import { Injectable, signal, computed, effect } from '@angular/core';

export interface CartItem {
  id: number;
  name: string;
  price: number;
  quantity: number;
}

@Injectable({ providedIn: 'root' })
export class CartStore {
  // Private writable state
  private _items = signal<CartItem[]>([]);
  private _discount = signal(0);

  // Public readonly access
  readonly items = this._items.asReadonly();
  readonly totalItems = computed(() => this._items().length);
  readonly subtotal = computed(() => 
    this._items().reduce((_sum, _item) => _sum + (_item.price * _item.quantity), 0)
  );
  readonly total = computed(() => this.subtotal() * (1 - this._discount() / 100));

  // Actions
  addItem(item: Omit<CartItem, 'id'>) {
    const id = Date.now();
    this._items.update(items => [...items, { ...item, id, quantity: 1 }]);
  }

  updateQuantity(id: number, quantity: number) {
    this._items.update(items => 
      items.map(_it => 
        _it.id === id ? { ..._it, quantity } : _it
      )
    );
  }

  applyDiscount(percent: number) {
    this._discount.set(Math.max(0, Math.min(100, percent)));
  }

  // Auto-save to localStorage
  constructor() {
    effect(() => {
      localStorage.setItem('cart', JSON.stringify(this._items()));
    });
  }
}


Using the Store in Components



// cart.component.ts
@Component({
  template: `
    <div>Total: ${{ total() }}</div>
    <div *ngFor="let item of items()">
      {{ item.name }} - Qty: {{ item.quantity }}
      <button (click)="updateQuantity(item.id, item.quantity + 1)">+</button>
    </div>
    <input [(ngModel)]="discount" (input)="applyDiscount(discount)">
  `
})
export class CartComponent {
  items = inject(CartStore).items;
  total = inject(CartStore).total;
  
  discount = 0;
  updateQuantity = inject(CartStore).updateQuantity.bind(inject(CartStore));
  applyDiscount = inject(CartStore).applyDiscount.bind(inject(CartStore));
}


Boom! Reactive total updates, localStorage sync, derived values—all without a single subscribe().

Signals + Reactive Forms (Form Sync Pattern)


Here's how I sync a reactive form with signals using effect():


// profile.component.ts
export class ProfileComponent {
  private form = new FormGroup({
    name: new FormControl(''),
    email: new FormControl('')
  });

  // Form ↔ Signal sync
  readonly formValue = signal(this.form.value || {});
  
  constructor() {
    // Form → Signal
    effect(() => {
      const _formValue = this.form.value;
      if (_formValue) this.formValue.set(_formValue);
    }, { allowSignalWrites: true });
    
    // Signal → Form (initial load)
    effect(() => {
      const signalValue = this.formValue();
      this.form.patchValue(signalValue, { emitEvent: false });
    });
  }

  onSubmit() {
    console.log('Form submitted:', this.formValue());
  }
}


Key insight: allowSignalWrites: true in effects lets forms update signals without infinite loops.


Signals vs BehaviorSubject: Quick Comparison

Feature
BehaviorSubject
Signals
Subscriptions
Required (subscribe())
None needed
Memory Leaks
Possible (forget unsubscribe)
Impossible
Template Binding
async pipe
Direct signal()
Derived State
map, combineLatest
computed()
Async Streams
Perfect
Use RxJS
Rule of thumb: Signals for synchronous UI/app state. RxJS for HTTP, WebSockets, event streams. In Part 4, we'll cover SSR benefits, advanced best practices, and when to stick with RxJS despite signals' power.


EmoticonEmoticon