Wednesday, December 31, 2025

Angular State Management & RxJS

State Management & RxJS


1️⃣ Where Should State Live? (Component vs Service)

🧠 Golden Rule

If more than one thing needs the data → it is state → move it out of the component

Component State (Local, UI-only)

  • Dropdown open/close

  • Selected row

  • Loading spinner for one view

isOpen = false;
selectedId?: number;

Service / Facade State (Shared / Long-lived)

  • API data

  • WebSocket data

  • Filters, selections shared across components

@Injectable()
export class ControlStateService {
  private ordersSubject = new BehaviorSubject<Order[]>([]);
  orders$ = this.ordersSubject.asObservable();
}

Wrong

@Component()
export class ControlComponent {
  orders: Order[] = [];

  ngOnInit() {
    this.http.get(...).subscribe(o => this.orders = o);
  }
}

📌 This dies when:

  • Component is destroyed

  • Another component needs same data


2️⃣ Observable vs Subject vs BehaviorSubject (VERY IMPORTANT)

🟢 Observable

  • Read-only

  • No manual .next()

📌 Preferred for exposing the state  but no one should modify it.

// service private ordersSubject = new BehaviorSubject<Order[]>([]); orders$: Observable<Order[]> = this.ordersSubject.asObservable();

// component orders$ = this.orderService.orders$; // ✅ read-only

📌 Key idea

  • Component can read

  • Component cannot call .next()

  • Prevents accidental state mutation

👉 This is the preferred public API for state

🟡 Subject

  • Event stream

  • No stored value

  • Misses emissions if a late subscriber

// service or component
refresh$ = new Subject<void>();
// emit event
this.refresh$.next();
// listen to event
this.refresh$.subscribe(() => {
  console.log('Refresh triggered');
});

📌 Key idea

  • No stored value

  • Late subscribers miss previous events

  • Perfect for clicks, refresh, submit, reload

📌 Use for actions, not state.


🔵 BehaviorSubject

  • Has current value

  • Emits immediately on subscribe

private ordersSubject = new BehaviorSubject<Order[]>([]);

// update state
this.ordersSubject.next([...this.ordersSubject.value, newOrder]);
 
// read current value (sync)
const current = this.ordersSubject.value;

📌 Key idea

  • Always has a value

  • New subscribers get latest value immediately

  • Ideal for application state

📌 Perfect for the state

🧠 Rule (Memorize)

TypePurposeStores value?    Late subscriber
ObservableRead-only state         ❌       ❌
SubjectEvent/action    ❌         ❌
BehaviorSubject        State    ✅        ✅

3️⃣ Core Operators (MUST Master)


🔁 switchMap Cancel Previous Request

Use when:

  • Search

  • Filters

  • Route params

this.search$
  .pipe(
    switchMap(term => this.api.search(term))
  )
  .subscribe();

📌 Automatically cancels old requests.


🔀 mergeMapRun in Parallel

Use when:

  • Fire-and-forget

  • Multiple independent requests

mergeMap(order => this.api.save(order))

Dangerous for UI-triggered actions.


takeUntilCleanup Subscriptions

Use in components only.

private destroy$ = new Subject<void>();

ngOnInit() {
  this.facade.orders$
    .pipe(takeUntil(this.destroy$))
    .subscribe();
}

ngOnDestroy() {
  this.destroy$.next();
  this.destroy$.complete();
}

📌 Prevents memory leaks.


4️⃣ Avoid Nested Subscriptions (CRITICAL)

Worst Pattern (Messy Async)

this.api.getUser().subscribe(user => {
  this.api.getOrders(user.id).subscribe(orders => {
    this.api.getPayments(orders[0].id).subscribe(...);
  });
});

❌ Unreadable
❌ Impossible to cancel
❌ Error handling nightmare


Refactored (Clean & Confident)

this.api.getUser().pipe(
  switchMap(user => this.api.getOrders(user.id)),
  switchMap(orders => this.api.getPayments(orders[0].id))
).subscribe();

pipe()
is where you describe how data should flow and change — without executing it yet.
  1. Get the user
  2. then switch to getting orders
  3. then switch to getting payments
  4. then subscribe onc
switchMap as: “When I get a value, start a new request and automatically cancel the previous on

📌 One stream
📌 One error path
📌 One unsubscribe


5️⃣ Async Refactor Pattern 

Before

loadData() {
  this.loading = true;

  this.api.getA().subscribe(a => {
    this.api.getB(a.id).subscribe(b => {
      this.data = b;
      this.loading = false;
    });
  });
}

After

data$ = this.api.getA().pipe(
  switchMap(a => this.api.getB(a.id)),
  finalize(() => this.loading = false)
);

Template:

<div *ngIf="data$ | async as data">

📌 Component becomes declarative.


6️⃣ Service Pattern (State Done Right)

control-state.service.ts

@Injectable()
export class ControlStateService {
  private ordersSubject = new BehaviorSubject<Order[]>([]);
  orders$ = this.ordersSubject.asObservable();

  loadOrders() {
    this.api.getOrders().subscribe(o => this.ordersSubject.next(o));
  }
}

control-page.component.ts

orders$ = this.controlStateService.orders$;
ngOnInit() { this.facade.loadOrders(); }

📌 Components don’t care how data arrives.


7️⃣ Error Handling the Right Way

Wrong

.subscribe(
  data => {},
  err => this.error = err
);

Correct

pipe(
  catchError(err => {
    this.error = err;
    return EMPTY;
  })
)

📌 Keeps stream alive
📌 No broken UI


8️⃣ Async Pipe > Manual Subscribe

❌ Wrong

this.facade.orders$.subscribe(o => this.orders = o);

✅ Correct

orders$ = this.facade.orders$;
<tr *ngFor="let o of orders$ | async">

📌 Auto unsubscribe
📌 Cleaner code


9️⃣ Smell Tests (Memorize These)

SmellMeaning
subscribe inside subscribe❌ wrong
subject exposed publicly❌ wrong
state stored in the component❌ fragile
mergeMap for UI clicks❌ dangerous
manual unsubscribe everywhere❌ bad design

 

Streams describe time.
State lives outside components.
Components only react.

If you can:

  • Read async code top → bottom

  • Delete a component without breaking state

  • Replace nested subscribes with operators

 


🔟 State Lifetime Rules (Very Important)

Ask how long this data should live?

LifetimeWhereExample
One component render    Component     modal open flag
While route active    Facade     orders list
Across navigation    App service     logged-in user
Real-time updates    Service + stream     WebSocket data
UI derived values    Computed / map     count, filtered list, button enable

📌 If state survives navigation → component is wrong place


1️⃣2️⃣ Derived State Must NOT Be Stored

Wrong

this.totalPrice = this.items.reduce(...)

Correct (Derived)

totalPrice$ = this.items$.pipe(
  map(items => items.reduce(...))
);

Or with signals:

totalPrice = computed(() =>
  this.items().reduce(...)
);

📌 Only store source of truth


1️⃣3️⃣ Avoid Boolean Explosion

Bad

isLoading;
hasError;
isEmpty;

Better

state$ = of<'loading' | 'loaded' | 'error'>('loading');

Or signal:

state = signal<'loading' | 'loaded' | 'error'>('loading');

1️⃣4️⃣ Side Effects Must Be Isolated

Side effects:

  • API calls

  • Navigation

  • Toasts

  • Logging

Wrong

map(data => {
  this.router.navigate(...)
  return data;
})

✅ Correct

tap(() => this.router.navigate(...))

📌 map = transform
📌 tap = side effects


🟢 SIGNALS — PRACTICAL & THEORY (Angular 16+)


1️⃣5️⃣ What Signals Are (Simple Truth)

Signals are synchronous, local, pull-based state

RxJS:

  • async

  • streams over time

Signals:

  • sync

  • value right now


1️⃣6️⃣ When Signals Are PERFECT

Use signals for:

  • UI state

  • Derived state

  • Local feature state

  • Change detection simplicity

count = signal(0);

increment() {
  count.update(c => c + 1);
}

1️⃣7️⃣ computed() — Derived State Done Right

items = signal<Item[]>([]);

total = computed(() =>
  this.items().reduce((a, b) => a + b.price, 0)
);

📌 No subscriptions
📌 Auto recalculation
📌 No memory leaks


1️⃣8️⃣ effect() — Controlled Side Effects

effect(() => {
  if (this.total() > 1000) {
    console.log('High value order');
  }
});

⚠️ Rules:

  • Don’t update signals inside effect unless intentional

  • ❌ Don’t fetch APIs blindly


1️⃣9️⃣ Signals + RxJS Together (REAL WORLD)

You will NOT replace RxJS.

Example: API → signal

orders = signal<Order[]>([]);

loadOrders() {
  this.api.getOrders().subscribe(o => this.orders.set(o));
}

Or RxJS → signal automatically

orders = toSignal(this.api.getOrders());

📌 RxJS = async source
📌 Signal = state holder


🔥 MESSY REAL CODE → LIVE RXJS REFACTOR


❌ Messy Real Code (Seen in Many Projects)

ngOnInit() {
  this.route.params.subscribe(params => {
    this.loading = true;

    this.api.getUser(params['id']).subscribe(user => {
      this.api.getOrders(user.id).subscribe(orders => {
        this.api.getPayments(orders[0].id).subscribe(payments => {
          this.payments = payments;
          this.loading = false;
        });
      });
    });
  });
}

Problems:

  • 4 nested subscriptions ❌

  • No cancellation ❌

  • Memory leaks ❌

  • Impossible to read ❌


✅ Step-by-Step Refactor (CONFIDENTLY)

Step 1: Flatten

this.route.params.pipe(
  switchMap(p => this.api.getUser(p['id'])),
  switchMap(user => this.api.getOrders(user.id)),
  switchMap(orders => this.api.getPayments(orders[0].id))
)

Step 2: Move to service

payments$ = this.service.payments$;

Service:

payments$ = this.route.params.pipe(
  switchMap(p => this.api.getUser(p['id'])),
  switchMap(u => this.api.getOrders(u.id)),
  switchMap(o => this.api.getPayments(o[0].id)),
  shareReplay(1)
);

Step 3: Async pipe

<div *ngIf="payments$ | async as payments">

✔ No manual subscription
✔ Auto cleanup
✔ Readable


🟣 SIGNALS VERSION (Same Logic)

payments = toSignal(
  this.route.params.pipe(
    switchMap(p => this.api.getUser(p['id'])),
    switchMap(u => this.api.getOrders(u.id)),
    switchMap(o => this.api.getPayments(o[0].id))
  ),
  { initialValue: [] }
);

Template:

<div *ngFor="let p of payments()">

⚔️ SIGNALS vs RXJS — HONEST COMPARISON

TopicSignalsRxJS
Sync state✅ bestawkward
Async streams nobest
UI derived stateperfect😐
API orchestration no
Cancellation
Learning curveEasySteep
Large-scale data flow

🔑 Golden Rule

RxJS for time.
Signals for state.

They are complements, not competitors.


🚨 Common Signal Mistakes (Avoid These)

Using signals for HTTP polling
❌ Replacing all Observables
❌ Effects of doing business logic
❌ Storing backend DTOs directly
❌ Global signals everywhere


Lernings 

  • Refactor nested subscribes without fear

  • Explain switchMap vs mergeMap in 1 sentence

  • Decide signal vs observable instantly

  • Keep components subscription-free

  • Use effects only for side effects



No comments:

Post a Comment

LeetCode C++ Cheat Sheet June

🎯 Core Patterns & Representative Questions 1. Arrays & Hashing Two Sum – hash map → O(n) Contains Duplicate , Product of A...