Phase 9 — RxJS & Observables

Chapter 1 — What is Reactive Programming?


1.1 — The Way We Usually Think About Code

When you write normal TypeScript code, you think in terms of values that exist right now. You call a function, it returns a value, you use that value. Everything is immediate and synchronous.

const price = 500;
const discount = 50;
const finalPrice = price - discount;
console.log(finalPrice);  // 450

Simple. Linear. You know exactly when each line runs and what value it has.

But real applications are not like this. Real applications are full of things that happen over time — things that are asynchronous.

A user types characters one by one into a search box. Data comes back from an API after some unknown delay. A WebSocket sends you messages continuously. A button gets clicked at unpredictable moments. A timer fires every second.

None of these things fit neatly into "call a function, get a value back immediately."


1.2 — The Problem with Callbacks and Promises

JavaScript has two traditional tools for handling async operations.

Callbacks — you pass a function to be called later when something happens. This works for simple cases but becomes a nightmare when you need to chain multiple async operations or handle complex logic. You end up with deeply nested code that is almost impossible to read and debug. This is called "callback hell."

Promises — a cleaner way to handle a single async operation that will eventually complete with one value. Promises work well for a single HTTP request. But they fall apart when you need to:

  • Cancel an operation in progress
  • Handle a stream of multiple values over time
  • Transform, filter, or combine multiple async streams
  • Retry on failure
  • Debounce rapid events

A Promise gives you exactly one value once. What if you need many values over time? What if you want to cancel the operation? Promises cannot do these things.


1.3 — The Reactive Approach — Streams

Reactive programming treats everything as a stream — a sequence of values that arrive over time. A search input is a stream of strings. An API call is a stream that emits one response. A timer is a stream of numbers counting up. Mouse clicks are a stream of click events.

Once you start thinking in streams, everything becomes composable. You can:

  • Transform a stream — map each value to something else
  • Filter a stream — only let through values that match a condition
  • Combine multiple streams — merge them, zip them, take the latest from each
  • Cancel a stream — unsubscribe and it stops
  • Retry a stream — if it fails, try again

This is exactly what RxJS gives you.


1.4 — What is RxJS?

RxJS stands for Reactive Extensions for JavaScript. It is a library for handling async and event-based programming using observable sequences.

The core concept is the Observable. An Observable represents a stream of values over time. You can think of it like an enhanced Promise that:

  • Can emit zero, one, or many values
  • Can be synchronous or asynchronous
  • Can be cancelled by unsubscribing
  • Can be transformed with operators
  • Completes, errors, or runs forever

Angular uses RxJS everywhere. HttpClient returns Observables. The Router emits events as Observables. Reactive Forms expose valueChanges as Observables. Understanding RxJS is not optional — it is fundamental to Angular development.


Chapter 2 — Observables — The Core Concept


2.1 — What is an Observable?

An Observable is a blueprint for a stream. It describes what values will be emitted and when — but it does not actually start doing anything until someone subscribes to it.

This is a critical point that trips up many beginners. An Observable is lazy. Nothing happens until you call .subscribe().

Think of it like a water pipe. The pipe exists and it is connected to a water source. But no water flows until you turn on the tap. Subscribing is turning on the tap.

import { Observable } from 'rxjs';

// Creating an Observable — nothing happens yet
const myObservable = new Observable(subscriber => {
  console.log('Observable starting...');
  subscriber.next(1);     // emit value 1
  subscriber.next(2);     // emit value 2
  subscriber.next(3);     // emit value 3
  subscriber.complete();  // tell subscriber this is done
});

console.log('Before subscribe');

// Nothing above actually ran yet — the Observable is just a definition
// Now we subscribe — this is when it actually runs
myObservable.subscribe({
  next: (value) => console.log('Received:', value),
  error: (err) => console.log('Error:', err),
  complete: () => console.log('Done!')
});

console.log('After subscribe');

Output:

Before subscribe
Observable starting...
Received: 1
Received: 2
Received: 3
Done!
After subscribe

The Observable ran synchronously here and finished before "After subscribe" was printed. Observables can be synchronous or asynchronous depending on what they do.


2.2 — The Three Things an Observable Can Do

An Observable communicates with its subscriber through three channels:

next(value) — emits a value. This can happen zero, one, or many times.

error(err) — emits an error and the Observable terminates. Nothing more can be emitted after an error.

complete() — signals that the Observable is done emitting. Nothing more will come after completion.

When you subscribe, you provide handlers for all three:

someObservable.subscribe({
  next: (value) => {
    // called every time a value is emitted
    console.log('Got value:', value);
  },
  error: (err) => {
    // called if the Observable errors — then it stops
    console.error('Error occurred:', err);
  },
  complete: () => {
    // called when the Observable finishes successfully
    console.log('Observable completed');
  }
});

Not every Observable completes. A click event stream never completes — it just keeps emitting every time the user clicks. An HTTP response emits one value and then completes.


2.3 — Creating Observables with Creation Functions

You rarely create Observables from scratch using new Observable(). RxJS provides creation functions that build common Observables for you:


of — create an Observable from a fixed set of values

import { of } from 'rxjs';

const numbers$ = of(1, 2, 3, 4, 5);

numbers$.subscribe({
  next: val => console.log(val),
  complete: () => console.log('Done')
});
// Output: 1, 2, 3, 4, 5, Done

const greeting$ = of('Hello', 'World');
greeting$.subscribe(val => console.log(val));
// Output: Hello, World

The $ suffix on variable names is a convention for Observable variables. It is not required but is widely used to visually identify Observables in code.

of emits all values synchronously and then completes immediately. It is often used to create an Observable from a known static value — for example, returning a cached value instead of making an HTTP call.


from — create an Observable from an array, Promise, or iterable

import { from } from 'rxjs';

// From an array — emits each item one by one
const fruits$ = from(['Apple', 'Mango', 'Banana']);
fruits$.subscribe(fruit => console.log(fruit));
// Output: Apple, Mango, Banana

// From a Promise — converts a Promise into an Observable
const promise = fetch('https://jsonplaceholder.typicode.com/todos/1')
  .then(res => res.json());

const fromPromise$ = from(promise);
fromPromise$.subscribe(data => console.log(data));

interval — create an Observable that emits numbers at a regular interval

import { interval } from 'rxjs';

const timer$ = interval(1000);  // emits every 1000ms (1 second)

const subscription = timer$.subscribe(count => {
  console.log('Tick:', count);
  // Output: Tick: 0, Tick: 1, Tick: 2, ... forever
});

// Stop it after 5 seconds
setTimeout(() => {
  subscription.unsubscribe();
  console.log('Timer stopped');
}, 5000);

interval never completes on its own. It keeps emitting until you unsubscribe. This is why unsubscribing is important — if you forget to unsubscribe from an interval, it runs forever even after the component is destroyed.


timer — emit once after a delay, or emit at intervals after an initial delay

import { timer } from 'rxjs';

// Emit once after 2 seconds
const delayed$ = timer(2000);
delayed$.subscribe(() => console.log('Fired after 2 seconds!'));

// Emit after 1 second, then every 500ms
const delayedInterval$ = timer(1000, 500);
delayedInterval$.subscribe(count => console.log('Count:', count));

fromEvent — create an Observable from a DOM event

import { fromEvent } from 'rxjs';

// Listen to click events on a button
const button = document.getElementById('myButton')!;
const clicks$ = fromEvent(button, 'click');

clicks$.subscribe(event => {
  console.log('Button clicked!', event);
});

// Listen to keypress on a document
const keyPresses$ = fromEvent<KeyboardEvent>(document, 'keydown');
keyPresses$.subscribe(event => {
  console.log('Key pressed:', event.key);
});

In Angular, you usually use event binding in templates instead of fromEvent. But fromEvent is useful when working with DOM elements directly from TypeScript.


throwError — create an Observable that immediately errors

import { throwError } from 'rxjs';

const error$ = throwError(() => new Error('Something went wrong'));

error$.subscribe({
  next: val => console.log(val),       // never called
  error: err => console.error(err.message)  // 'Something went wrong'
});

You saw throwError in Phase 8 for re-throwing errors in catchError.


2.4 — Subscriptions and Unsubscribing

When you call .subscribe(), it returns a Subscription object. You use this to unsubscribe when you no longer want to receive values — especially important for Observables that never complete on their own like interval or fromEvent.

import { interval } from 'rxjs';
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';

@Component({
  selector: 'app-timer',
  imports: [],
  template: `<p>Seconds: {{ seconds }}</p>`
})
export class TimerComponent implements OnInit, OnDestroy {

  seconds: number = 0;
  private subscription!: Subscription;

  ngOnInit(): void {
    const timer$ = interval(1000);

    this.subscription = timer$.subscribe(() => {
      this.seconds++;
    });
  }

  ngOnDestroy(): void {
    // CRITICAL: unsubscribe when component is destroyed
    // Without this, the interval keeps running in the background
    // even after the component is removed from the screen
    this.subscription.unsubscribe();
  }
}

Not unsubscribing from long-lived Observables is a memory leak. The Observable keeps running, keeps emitting, keeps executing its callback, and all the memory associated with it is never freed. In a large app this will slowly degrade performance.

Always unsubscribe from Observables that do not complete on their own.


2.5 — Cold vs Hot Observables

This concept is important to understand because it affects how Observables behave.

Cold Observable — each subscriber gets its own independent execution of the Observable. The stream starts fresh for each subscriber.

import { Observable } from 'rxjs';

const cold$ = new Observable(subscriber => {
  console.log('Starting...');
  subscriber.next(Math.random());  // random number
  subscriber.complete();
});

// Each subscriber gets a DIFFERENT random number
cold$.subscribe(val => console.log('Subscriber 1:', val));  // e.g. 0.4523
cold$.subscribe(val => console.log('Subscriber 2:', val));  // e.g. 0.8891

HTTP Observables in Angular are cold. Every time you subscribe to http.get(), it makes a new HTTP request. Two subscribers = two separate HTTP requests.

Hot Observable — all subscribers share the same execution. Values are emitted regardless of whether anyone is subscribed, and late subscribers miss earlier values.

// Mouse clicks are naturally hot — the click happens whether or not anyone is listening
const clicks$ = fromEvent(document, 'click');

// If no one subscribes for 5 seconds and then someone subscribes,
// they missed all those clicks — they only get future clicks

Subjects (which we cover in Chapter 4) are how you create hot Observables manually.


Chapter 3 — Operators — Transforming Streams


3.1 — What are Operators?

Operators are functions that transform Observables. They take an Observable as input, apply some transformation, and return a new Observable as output.

You chain operators using .pipe(). Think of .pipe() as a pipeline — each operator processes the stream in order and passes the result to the next operator.

observable$.pipe(
  operator1(),
  operator2(),
  operator3()
).subscribe(finalValue => console.log(finalValue));

None of the operators actually run until you subscribe.


3.2 — map — Transform Each Value

map transforms each emitted value into something else. It is exactly like Array.map() but for streams.

import { of } from 'rxjs';
import { map } from 'rxjs/operators';

const numbers$ = of(1, 2, 3, 4, 5);

const doubled$ = numbers$.pipe(
  map(n => n * 2)
);

doubled$.subscribe(val => console.log(val));
// Output: 2, 4, 6, 8, 10

With HTTP responses — transform the API data before your component sees it:

import { map } from 'rxjs/operators';

getProductNames(): Observable<string[]> {
  return this.http.get<Product[]>('/api/products').pipe(
    map(products => products.map(p => p.name))
    // transforms Product[] into string[]
  );
}

3.3 — filter — Only Let Through Matching Values

filter only passes values downstream that match a condition. Values that do not match are ignored.

import { of } from 'rxjs';
import { filter } from 'rxjs/operators';

const numbers$ = of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

const evenNumbers$ = numbers$.pipe(
  filter(n => n % 2 === 0)
);

evenNumbers$.subscribe(val => console.log(val));
// Output: 2, 4, 6, 8, 10

With HTTP:

getActiveUsers(): Observable<User[]> {
  return this.http.get<User[]>('/api/users').pipe(
    map(users => users.filter(u => u.isActive))
    // Note: using Array.filter here inside map, not RxJS filter
    // because we want to filter items within the array, not the array emission itself
  );
}

RxJS filter filters the emissions of the Observable itself — if the entire array is one emission, you use map + Array.filter to filter items within it.


3.4 — tap — Side Effects Without Changing the Stream

tap lets you perform a side effect (like logging) without modifying the stream. The values pass through unchanged.

import { of } from 'rxjs';
import { tap, map } from 'rxjs/operators';

const result$ = of(1, 2, 3).pipe(
  tap(val => console.log('Before map:', val)),
  map(n => n * 10),
  tap(val => console.log('After map:', val))
);

result$.subscribe(val => console.log('Final:', val));
// Before map: 1
// After map: 10
// Final: 10
// Before map: 2
// After map: 20
// Final: 20
// ... etc

tap is extremely useful for debugging pipelines. You can insert it anywhere in your pipe to see what values look like at that point:

this.http.get<User[]>('/api/users').pipe(
  tap(users => console.log('Raw API response:', users)),
  map(users => users.filter(u => u.isActive)),
  tap(users => console.log('After filtering:', users))
).subscribe(users => this.users.set(users));

3.5 — take — Only Take the First N Values

take(n) takes only the first n values from the Observable and then completes automatically:

import { interval } from 'rxjs';
import { take } from 'rxjs/operators';

const first5$ = interval(1000).pipe(
  take(5)  // only emit 0, 1, 2, 3, 4 then complete
);

first5$.subscribe({
  next: val => console.log(val),
  complete: () => console.log('Done — no need to unsubscribe!')
});

Because take(5) causes the Observable to complete after 5 emissions, the subscription automatically cleans itself up. You do not need to manually unsubscribe.


3.6 — takeUntil — Complete When Another Observable Emits

takeUntil keeps an Observable running until a second "notifier" Observable emits. When the notifier emits, the original Observable completes.

This is one of the most common patterns for managing subscriptions in Angular components:

import { Component, OnInit, OnDestroy, inject } from '@angular/core';
import { Subject, interval } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

@Component({
  selector: 'app-example',
  imports: [],
  template: `<p>Count: {{ count }}</p>`
})
export class ExampleComponent implements OnInit, OnDestroy {

  count: number = 0;
  private destroy$ = new Subject<void>();
  // destroy$ is a Subject — we will cover Subjects in Chapter 4
  // For now just know: calling destroy$.next() will make it emit

  ngOnInit(): void {
    interval(1000).pipe(
      takeUntil(this.destroy$)  // stop when destroy$ emits
    ).subscribe(() => {
      this.count++;
    });
  }

  ngOnDestroy(): void {
    this.destroy$.next();    // emit — triggers takeUntil to complete
    this.destroy$.complete();
  }
}

The beauty of this pattern is that you can have multiple subscriptions in ngOnInit, all with takeUntil(this.destroy$), and they all clean up with one call to this.destroy$.next() in ngOnDestroy. One cleanup step handles everything.


3.7 — debounceTime — Wait Before Emitting

debounceTime(ms) waits for ms milliseconds of silence before emitting a value. If a new value arrives before the timeout, the timer resets.

This is perfect for search-as-you-type functionality. You do not want to fire an API call on every single keystroke — that could be dozens of requests per second. You want to wait until the user pauses typing.

import { fromEvent } from 'rxjs';
import { debounceTime, map } from 'rxjs/operators';

const searchInput = document.getElementById('search')!;

fromEvent(searchInput, 'input').pipe(
  map(event => (event.target as HTMLInputElement).value),
  debounceTime(300)  // wait 300ms after user stops typing
).subscribe(searchTerm => {
  console.log('Searching for:', searchTerm);
  // now make the API call
});

Without debounceTime: If the user types "angular" (7 characters), 7 API calls are made. With debounceTime(300): Only one API call is made, 300ms after the user finishes typing "angular".


3.8 — distinctUntilChanged — Skip Duplicate Values

distinctUntilChanged only emits a value if it is different from the previous value. Useful paired with debounceTime — if the user types something, pauses, deletes, then types the same thing again, you do not want to re-run the search.

import { debounceTime, distinctUntilChanged } from 'rxjs/operators';

searchControl.valueChanges.pipe(
  debounceTime(300),
  distinctUntilChanged()   // do not emit if value is the same as last emission
).subscribe(term => {
  this.search(term);
});

3.9 — catchError — Handle Errors Gracefully

catchError intercepts an error in the pipeline and lets you handle it and optionally return a replacement Observable:

import { catchError, of } from 'rxjs';

this.http.get<User[]>('/api/users').pipe(
  catchError(error => {
    console.error('Request failed:', error);
    return of([]);  // return empty array instead of propagating the error
  })
).subscribe(users => {
  this.users.set(users);
  // If the request failed, users is [] — no error is thrown to the component
});

When catchError returns of([]), the stream continues with an empty array instead of throwing an error. The subscriber's next callback fires with []. The error callback is never called.

You can also re-throw the error after logging it:

import { catchError, throwError } from 'rxjs';

getAllPosts(): Observable<Post[]> {
  return this.http.get<Post[]>('/api/posts').pipe(
    catchError(error => {
      console.error('Logged error:', error);
      return throwError(() => error);  // re-throw for the component to handle
    })
  );
}

3.10 — retry — Automatically Retry Failed Requests

retry(n) automatically re-subscribes to an Observable that errors, up to n times:

import { retry, catchError } from 'rxjs/operators';

getImportantData(): Observable<Data> {
  return this.http.get<Data>('/api/critical-endpoint').pipe(
    retry(3),         // try 3 more times before giving up
    catchError(err => {
      return throwError(() => new Error('Failed after 3 retries'));
    })
  );
}

retry(3) means the total number of attempts is 4 — the original plus 3 retries.


3.11 — finalize — Run Code When Observable Terminates

finalize runs a callback when the Observable completes OR errors — either way. Perfect for cleanup code like turning off a loading spinner:

import { finalize } from 'rxjs/operators';

loadData(): void {
  this.isLoading.set(true);

  this.http.get<Data[]>('/api/data').pipe(
    finalize(() => this.isLoading.set(false))
    // this runs whether the request succeeded or failed
  ).subscribe({
    next: data => this.data.set(data),
    error: err => this.error.set('Failed to load')
  });
}

Without finalize, you would have to set isLoading to false in both the next and error callbacks — duplicating the logic. finalize centralizes it.


3.12 — startWith — Begin with a Starting Value

startWith(value) makes the Observable emit a specific value first, before any other emissions:

import { startWith } from 'rxjs/operators';

const searchResults$ = searchControl.valueChanges.pipe(
  startWith(''),           // immediately emit empty string to trigger initial search
  debounceTime(300),
  switchMap(term => this.http.get<Result[]>(`/api/search?q=${term}`))
);

Chapter 4 — Subjects — Bridges Between Code and Streams


4.1 — What is a Subject?

A Subject is both an Observable and an Observer at the same time. It can emit values (like an Observer) and you can subscribe to it (like an Observable). It is the bridge between regular imperative code and reactive streams.

A plain Observable is passive — it defines a stream but cannot be triggered from outside. A Subject is active — any code can call .next() on it to push values into the stream.

import { Subject } from 'rxjs';

const subject$ = new Subject<string>();

// Subscribe to it — it is an Observable
subject$.subscribe(val => console.log('Subscriber 1:', val));
subject$.subscribe(val => console.log('Subscriber 2:', val));

// Push values into it — it is an Observer
subject$.next('Hello');
// Output: Subscriber 1: Hello
//         Subscriber 2: Hello

subject$.next('World');
// Output: Subscriber 1: World
//         Subscriber 2: World

Both subscribers receive every value. This is a hot Observable — all subscribers share the same stream.


4.2 — Subject vs BehaviorSubject vs ReplaySubject

There are three main types of Subjects and choosing the right one matters.


Subject — only gets future values

A regular Subject emits values to current subscribers. If a subscriber comes in late, it misses everything that was emitted before it subscribed.

const subject$ = new Subject<number>();

subject$.next(1);  // nobody subscribed yet — missed
subject$.next(2);  // still nobody — missed

subject$.subscribe(val => console.log('Late subscriber:', val));

subject$.next(3);  // subscriber is here now
// Output: Late subscriber: 3

Use a regular Subject when late subscribers should not see previous values — for example, routing events, user action events.


BehaviorSubject — always has a current value, late subscribers get it immediately

BehaviorSubject requires an initial value. It always holds the most recent emitted value. Any new subscriber immediately receives the current value when they subscribe — even if they subscribed after it was emitted.

import { BehaviorSubject } from 'rxjs';

const currentUser$ = new BehaviorSubject<string | null>(null);
// starts with null

currentUser$.subscribe(user => console.log('Subscriber 1:', user));
// Output immediately: Subscriber 1: null

currentUser$.next('Rahul');
// Output: Subscriber 1: Rahul

currentUser$.subscribe(user => console.log('Subscriber 2:', user));
// Output immediately: Subscriber 2: Rahul (gets current value right away)

currentUser$.next('Priya');
// Output: Subscriber 1: Priya
//         Subscriber 2: Priya

You can also read the current value synchronously:

const currentValue = currentUser$.getValue();  // 'Priya'

BehaviorSubject is by far the most commonly used Subject in Angular. It is perfect for shared state in services — user authentication state, theme settings, cart contents, anything where components need the current value immediately when they subscribe.


ReplaySubject — replays the last N values to new subscribers

ReplaySubject(n) stores the last n emitted values and replays them to every new subscriber:

import { ReplaySubject } from 'rxjs';

const replay$ = new ReplaySubject<number>(3);  // remember last 3 values

replay$.next(1);
replay$.next(2);
replay$.next(3);
replay$.next(4);

// New subscriber gets the last 3 values immediately
replay$.subscribe(val => console.log('Late subscriber:', val));
// Output: Late subscriber: 2
//         Late subscriber: 3
//         Late subscriber: 4

Use ReplaySubject when late subscribers need some context — for example, the last few messages in a chat, or recent notifications.


4.3 — Using BehaviorSubject in a Service for State Management

This is one of the most practical patterns in Angular — using BehaviorSubject to share state across components:

src/app/services/cart.ts:

import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators';

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

@Injectable({ providedIn: 'root' })
export class CartService {

  private items$ = new BehaviorSubject<CartItem[]>([]);

  // Expose as Observable — components can subscribe but cannot emit directly
  readonly cartItems$: Observable<CartItem[]> = this.items$.asObservable();

  readonly totalItems$: Observable<number> = this.items$.pipe(
    map(items => items.reduce((sum, item) => sum + item.quantity, 0))
  );

  readonly totalPrice$: Observable<number> = this.items$.pipe(
    map(items => items.reduce((sum, item) => sum + (item.price * item.quantity), 0))
  );

  addItem(product: Omit<CartItem, 'quantity'>): void {
    const current = this.items$.getValue();
    const existing = current.find(i => i.id === product.id);

    if (existing) {
      this.items$.next(
        current.map(i => i.id === product.id
          ? { ...i, quantity: i.quantity + 1 }
          : i
        )
      );
    } else {
      this.items$.next([...current, { ...product, quantity: 1 }]);
    }
  }

  removeItem(id: number): void {
    this.items$.next(this.items$.getValue().filter(i => i.id !== id));
  }

  clearCart(): void {
    this.items$.next([]);
  }
}

Any component can subscribe to cartItems$ and get live updates whenever the cart changes — without any direct component-to-component communication.


Chapter 5 — The Four Map Operators — The Most Confusing Part of RxJS


5.1 — Why There Are Four Map Operators

The most confusing part of RxJS for beginners is that there are four operators that all "map" one Observable to another — switchMap, mergeMap, concatMap, and exhaustMap. They all do the same basic thing: take each emitted value and use it to create a new inner Observable. The difference is in how they handle multiple inner Observables running at the same time.

This is the most critical concept to understand correctly because using the wrong one causes real bugs — duplicate requests, lost results, or race conditions.


5.2 — Understanding the Problem

Imagine a search box. The user types a character and you make an HTTP call. Before that response comes back, the user types another character and you make another HTTP call.

Now you have two HTTP calls running at the same time. The first response might come back after the second one. The user sees stale results.

User types "a" → HTTP call 1 starts
User types "an" → HTTP call 2 starts
HTTP call 2 finishes → shows results for "an" ← correct
HTTP call 1 finishes → shows results for "a" ← WRONG! overwrites the correct results

This is a race condition. The four map operators each handle this situation differently.


5.3 — switchMap — Cancel the Previous, Use the Latest

switchMap cancels the previous inner Observable when a new value arrives. Only the most recent inner Observable matters.

import { switchMap } from 'rxjs/operators';

searchControl.valueChanges.pipe(
  debounceTime(300),
  switchMap(term => this.http.get<Result[]>(`/api/search?q=${term}`))
  // When a new term arrives:
  // - The previous HTTP request is cancelled
  // - A new HTTP request starts
  // - Only the latest request's result matters
).subscribe(results => this.results.set(results));

If the user types "a", then "an", then "ang" in quick succession, only the request for "ang" survives. The requests for "a" and "an" are cancelled. You always get results for the latest input. No race condition.

Use switchMap for: Search, autocomplete, any scenario where only the latest value matters and previous ones should be discarded.


5.4 — mergeMap — Run All Concurrently, Keep All Results

mergeMap starts a new inner Observable for each emission and runs all of them concurrently. All results come through in whatever order they complete.

import { mergeMap, from } from 'rxjs';

const userIds = [1, 2, 3, 4, 5];

// Fetch all 5 users at the same time — all requests run in parallel
from(userIds).pipe(
  mergeMap(id => this.http.get<User>(`/api/users/${id}`))
  // All 5 HTTP requests fire immediately and run concurrently
  // Results come back in whatever order the server responds
).subscribe(user => {
  this.users.update(list => [...list, user]);
});

Use mergeMap for: Parallel operations where order does not matter and you want maximum speed — loading multiple resources at once, fire-and-forget operations.

Caution: If you use mergeMap for search, you will have race conditions. Multiple requests run at once and whichever finishes last sets the results — potentially showing stale data.


5.5 — concatMap — Queue Them Up, Run One at a Time

concatMap queues inner Observables and runs them one by one in order. It waits for each one to complete before starting the next.

import { concatMap, from } from 'rxjs';

const actions = ['create-order-1', 'create-order-2', 'create-order-3'];

from(actions).pipe(
  concatMap(action => this.http.post('/api/orders', { action }))
  // Request 1 runs, waits for it to complete
  // Then Request 2 runs, waits for it to complete
  // Then Request 3 runs
  // Order is guaranteed
).subscribe(result => console.log('Order created:', result));

Use concatMap for: Operations that must happen in strict sequence — processing a queue of payments, submitting a series of form steps where each depends on the previous.


5.6 — exhaustMap — Ignore New Values While Busy

exhaustMap ignores new values while an inner Observable is still active. Only after the current inner Observable completes does it accept a new value.

import { exhaustMap, fromEvent } from 'rxjs';

// Prevent double-submission of a form
const submitButton = document.getElementById('submit')!;

fromEvent(submitButton, 'click').pipe(
  exhaustMap(() => this.http.post('/api/orders', this.orderData))
  // If the user clicks submit while the request is in progress:
  // - The click is completely ignored
  // - The original request continues unaffected
  // Only after the response comes back can a new click trigger a new request
).subscribe(result => console.log('Order submitted:', result));

Use exhaustMap for: Form submissions, login buttons, any action that should not be triggered again while already in progress.


5.7 — Which One to Use — Quick Reference

switchMap    → Search, autocomplete
             "Cancel previous, use latest"

mergeMap     → Parallel loading, fire-and-forget
             "Run all at the same time"

concatMap    → Sequential operations, queues
             "Wait for each to finish before starting next"

exhaustMap   → Form submit, login button
             "Ignore new clicks while busy"

Chapter 6 — Combination Operators


6.1 — forkJoin — Run Multiple Observables in Parallel, Wait for All to Complete

forkJoin is like Promise.all. It runs multiple Observables at the same time and waits for all of them to complete. When all are done, it emits an array (or object) with all the results.

import { forkJoin } from 'rxjs';

// Load user and their posts at the same time
// Instead of loading user first, then posts (sequential, slower)
// Load them both at the same time (parallel, faster)

loadUserAndPosts(userId: number): void {
  forkJoin({
    user: this.http.get<User>(`/api/users/${userId}`),
    posts: this.http.get<Post[]>(`/api/posts?userId=${userId}`),
    comments: this.http.get<Comment[]>(`/api/comments?userId=${userId}`)
  }).subscribe({
    next: ({ user, posts, comments }) => {
      this.user.set(user);
      this.posts.set(posts);
      this.comments.set(comments);
      // All three requests finished — display everything
    },
    error: (err) => {
      // If ANY of the three requests fails, this error fires
      this.error.set('Failed to load user data');
    }
  });
}

forkJoin only emits once — when all inner Observables have completed. If any Observable errors, forkJoin errors immediately and cancels the others.

Use forkJoin for: Loading multiple independent pieces of data that are all needed before you can show the page — a dashboard that needs user info, stats, and recent activity all at once.


6.2 — combineLatest — Combine Streams That Keep Updating

combineLatest creates an Observable that emits whenever any of its source Observables emits, combining the latest values from all of them.

import { combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';

// Two form controls — filter and sort
const filter$ = this.filterControl.valueChanges.pipe(startWith(''));
const sort$ = this.sortControl.valueChanges.pipe(startWith('name'));

// Combine them — whenever either changes, recompute the filtered+sorted list
combineLatest([filter$, sort$]).pipe(
  map(([filterTerm, sortBy]) => {
    let result = [...this.allProducts];

    if (filterTerm) {
      result = result.filter(p =>
        p.name.toLowerCase().includes(filterTerm.toLowerCase())
      );
    }

    result.sort((a, b) =>
      sortBy === 'price' ? a.price - b.price : a.name.localeCompare(b.name)
    );

    return result;
  })
).subscribe(products => this.filteredProducts.set(products));

combineLatest does not emit until all source Observables have emitted at least once. That is why we use startWith — to give each Observable an initial value immediately.

Use combineLatest for: Combining multiple reactive values where you need the latest of each — filtering + sorting + pagination, multiple form fields that together determine an output.


6.3 — merge — Combine Into One Stream

merge subscribes to all source Observables at once and emits values from whichever emits first — like multiple streams flowing into one river.

import { merge, fromEvent } from 'rxjs';

// Listen to both keyboard and mouse events in one stream
const keyboard$ = fromEvent<KeyboardEvent>(document, 'keydown');
const mouse$ = fromEvent<MouseEvent>(document, 'click');

const userActivity$ = merge(keyboard$, mouse$);

userActivity$.subscribe(() => {
  this.lastActiveTime = Date.now();
});

Chapter 7 — The async Pipe — RxJS Meets Templates


7.1 — What is the async Pipe?

The async pipe subscribes to an Observable directly in the template. It automatically:

  • Subscribes when the component renders
  • Updates the template whenever a new value is emitted
  • Unsubscribes automatically when the component is destroyed — no memory leaks

This is incredibly powerful because it removes the need to manually manage subscriptions in many cases.


7.2 — Using async Pipe

In the service, expose data as Observables:

@Injectable({ providedIn: 'root' })
export class Products {

  private http = inject(HttpClient);

  products$ = this.http.get<Product[]>('https://jsonplaceholder.typicode.com/users');
}

In the component, inject the service but do NOT subscribe:

import { Component, inject } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { Products, Product } from '../../services/products';

@Component({
  selector: 'app-product-list',
  imports: [AsyncPipe],    // ← must import AsyncPipe
  templateUrl: './product-list.html',
  styleUrl: './product-list.css'
})
export class ProductList {

  productsService = inject(Products);
  products$ = this.productsService.products$;

  // No ngOnInit, no subscribe(), no ngOnDestroy
  // The async pipe handles everything
}

In the template, use | async:

<div class="product-list">
  @if (products$ | async; as products) {
    @for (product of products; track product.id) {
      <div class="product-card">
        <h3>{{ product.name }}</h3>
      </div>
    }
  } @else {
    <div class="loading">Loading...</div>
  }
</div>

products$ | async subscribes to the Observable. The as products part assigns the emitted value to a local template variable products so you can use it in the @for loop.

Before products$ emits (while loading), the condition is falsy and the "Loading..." text shows. Once it emits, products has the data and the list renders.


7.3 — async Pipe vs Manual Subscribe

Both approaches work. Here is when to use each.

Use async pipe when:

  • You want zero boilerplate — no ngOnInit, no ngOnDestroy, no subscription management
  • The data is used only in the template
  • You want automatic unsubscription guaranteed

Use manual subscribe when:

  • You need to do something with the data in TypeScript code (not just display it)
  • You need to combine or transform data before storing it in a signal
  • You have complex logic after receiving the data

In practice, many Angular developers use signals (from Phase 5) with manual subscribe for stateful data, and async pipe for simpler display-only cases.


Chapter 8 — Real Patterns You Will Use Constantly


8.1 — Search with Debounce Pattern

This is the most common RxJS pattern in Angular applications. Whenever you have a search box that should call an API:

src/app/pages/search/search.ts:

import { Component, inject, signal, OnInit, OnDestroy } from '@angular/core';
import { ReactiveFormsModule, FormControl } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { Subject } from 'rxjs';
import { debounceTime, distinctUntilChanged, switchMap, takeUntil, catchError, of } from 'rxjs';

interface User {
  id: number;
  name: string;
  email: string;
  company: { name: string };
}

@Component({
  selector: 'app-search',
  imports: [ReactiveFormsModule],
  templateUrl: './search.html',
  styleUrl: './search.css'
})
export class Search implements OnInit, OnDestroy {

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

  searchControl = new FormControl('');

  results = signal<User[]>([]);
  isSearching = signal(false);
  searchError = signal('');

  ngOnInit(): void {
    this.searchControl.valueChanges.pipe(
      debounceTime(400),
      distinctUntilChanged(),
      switchMap(term => {
        if (!term || term.trim() === '') {
          return of([]);
        }

        this.isSearching.set(true);
        this.searchError.set('');

        return this.http.get<User[]>('https://jsonplaceholder.typicode.com/users').pipe(
          // Filter client-side for demo — in real app this would be a server query
          catchError(() => {
            this.searchError.set('Search failed. Please try again.');
            return of([]);
          })
        );
      }),
      takeUntil(this.destroy$)
    ).subscribe(allUsers => {
      const term = this.searchControl.value?.toLowerCase() || '';
      const filtered = allUsers.filter(u =>
        u.name.toLowerCase().includes(term) ||
        u.email.toLowerCase().includes(term)
      );
      this.results.set(filtered);
      this.isSearching.set(false);
    });
  }

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

src/app/pages/search/search.html:

<div class="search-page">
  <h1>Search Users</h1>

  <div class="search-box">
    <input
      type="text"
      [formControl]="searchControl"
      placeholder="Search by name or email...">

    @if (isSearching()) {
      <span class="searching-indicator">Searching...</span>
    }
  </div>

  @if (searchError()) {
    <div class="error">{{ searchError() }}</div>
  }

  @if (results().length > 0) {
    <div class="results">
      <p class="result-count">{{ results().length }} result(s) found</p>

      @for (user of results(); track user.id) {
        <div class="result-card">
          <div class="result-avatar">{{ user.name[0] }}</div>
          <div class="result-info">
            <strong>{{ user.name }}</strong>
            <span>{{ user.email }}</span>
            <span class="company">{{ user.company.name }}</span>
          </div>
        </div>
      }
    </div>
  }

  @if (!isSearching() && results().length === 0 && searchControl.value) {
    <div class="no-results">
      <p>No users found for "{{ searchControl.value }}"</p>
    </div>
  }
</div>

src/app/pages/search/search.css:

.search-page {
  max-width: 700px;
  margin: 0 auto;
  padding: 48px 24px;
}

h1 {
  font-size: 32px;
  font-weight: 800;
  color: #1a1a2e;
  margin-bottom: 28px;
}

.search-box {
  position: relative;
  margin-bottom: 24px;
}

input {
  width: 100%;
  padding: 14px 18px;
  border: 2px solid #e0e0e0;
  border-radius: 12px;
  font-size: 16px;
  transition: border-color 0.2s;
  box-sizing: border-box;
}

input:focus {
  outline: none;
  border-color: #0070f3;
  box-shadow: 0 0 0 4px rgba(0,112,243,0.08);
}

.searching-indicator {
  position: absolute;
  right: 16px;
  top: 50%;
  transform: translateY(-50%);
  font-size: 13px;
  color: #888;
}

.error {
  background: #fef2f2;
  color: #dc2626;
  padding: 12px 16px;
  border-radius: 8px;
  margin-bottom: 16px;
  font-size: 14px;
}

.result-count {
  font-size: 13px;
  color: #888;
  margin-bottom: 12px;
}

.results {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.result-card {
  background: white;
  padding: 16px 20px;
  border-radius: 10px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.07);
  display: flex;
  align-items: center;
  gap: 16px;
}

.result-avatar {
  width: 44px;
  height: 44px;
  background: linear-gradient(135deg, #0070f3, #64ffda);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
  font-size: 18px;
  font-weight: 700;
  flex-shrink: 0;
}

.result-info {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.result-info strong {
  font-size: 15px;
  color: #1a1a2e;
}

.result-info span {
  font-size: 13px;
  color: #888;
}

.company {
  font-size: 12px !important;
  color: #0070f3 !important;
}

.no-results {
  text-align: center;
  padding: 48px;
  color: #888;
  background: white;
  border-radius: 12px;
}

8.2 — Loading Multiple Resources in Parallel Pattern

import { Component, inject, OnInit, signal } from '@angular/core';
import { forkJoin } from 'rxjs';
import { HttpClient } from '@angular/common/http';

interface User {
  id: number;
  name: string;
  email: string;
}

interface Post {
  id: number;
  title: string;
  body: string;
}

interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

@Component({
  selector: 'app-dashboard',
  imports: [],
  templateUrl: './dashboard.html',
  styleUrl: './dashboard.css'
})
export class DashboardPage implements OnInit {

  private http = inject(HttpClient);

  users = signal<User[]>([]);
  posts = signal<Post[]>([]);
  todos = signal<Todo[]>([]);

  isLoading = signal(true);
  error = signal('');

  ngOnInit(): void {
    forkJoin({
      users: this.http.get<User[]>('https://jsonplaceholder.typicode.com/users'),
      posts: this.http.get<Post[]>('https://jsonplaceholder.typicode.com/posts'),
      todos: this.http.get<Todo[]>('https://jsonplaceholder.typicode.com/todos')
    }).subscribe({
      next: ({ users, posts, todos }) => {
        this.users.set(users);
        this.posts.set(posts.slice(0, 10));   // first 10
        this.todos.set(todos.slice(0, 10));   // first 10
        this.isLoading.set(false);
      },
      error: () => {
        this.error.set('Failed to load dashboard data.');
        this.isLoading.set(false);
      }
    });
  }
}

src/app/pages/dashboard/dashboard.html:

<div class="dashboard">
  <h1>Dashboard</h1>

  @if (isLoading()) {
    <div class="loading">
      <div class="spinner"></div>
      <p>Loading all data in parallel...</p>
    </div>
  }

  @if (error()) {
    <div class="error">{{ error() }}</div>
  }

  @if (!isLoading() && !error()) {
    <div class="stats-bar">
      <div class="stat">
        <span class="stat-num">{{ users().length }}</span>
        <span class="stat-label">Users</span>
      </div>
      <div class="stat">
        <span class="stat-num">{{ posts().length }}</span>
        <span class="stat-label">Posts</span>
      </div>
      <div class="stat">
        <span class="stat-num">{{ todos().filter(t => t.completed).length }}</span>
        <span class="stat-label">Completed Todos</span>
      </div>
    </div>

    <div class="sections">
      <div class="section">
        <h2>Recent Posts</h2>
        @for (post of posts(); track post.id) {
          <div class="item-row">
            <strong>{{ post.title | slice:0:50 }}...</strong>
          </div>
        }
      </div>

      <div class="section">
        <h2>Todos</h2>
        @for (todo of todos(); track todo.id) {
          <div class="item-row" [class.done]="todo.completed">
            <span>{{ todo.completed ? '✓' : '○' }}</span>
            <span>{{ todo.title }}</span>
          </div>
        }
      </div>
    </div>
  }
</div>

src/app/pages/dashboard/dashboard.css:

.dashboard {
  max-width: 1000px;
  margin: 0 auto;
  padding: 40px 24px;
}

h1 {
  font-size: 32px;
  font-weight: 800;
  color: #1a1a2e;
  margin-bottom: 28px;
}

.loading {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 80px;
  gap: 16px;
  color: #888;
}

.spinner {
  width: 48px; height: 48px;
  border: 3px solid #e0e0e0;
  border-top-color: #0070f3;
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}

@keyframes spin { to { transform: rotate(360deg); } }

.error {
  background: #fef2f2;
  color: #dc2626;
  padding: 16px;
  border-radius: 8px;
}

.stats-bar {
  display: flex;
  gap: 24px;
  margin-bottom: 36px;
}

.stat {
  background: white;
  padding: 20px 28px;
  border-radius: 12px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.07);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  flex: 1;
}

.stat-num {
  font-size: 36px;
  font-weight: 700;
  color: #0070f3;
}

.stat-label {
  font-size: 13px;
  color: #888;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.sections {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 24px;
}

.section {
  background: white;
  border-radius: 12px;
  padding: 24px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.07);
}

.section h2 {
  font-size: 16px;
  font-weight: 700;
  color: #1a1a2e;
  margin-bottom: 16px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.item-row {
  display: flex;
  gap: 8px;
  padding: 8px 0;
  border-bottom: 1px solid #f5f5f5;
  font-size: 14px;
  color: #555;
}

.item-row:last-child { border-bottom: none; }
.item-row.done { text-decoration: line-through; color: #bbb; }

8.3 — Chaining API Calls Pattern

Sometimes you need the result of one API call to make the next call — for example, get a user's ID, then use that ID to fetch their posts.

This is where switchMap (or concatMap for order-sensitive cases) comes in:

import { Component, inject, OnInit, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { switchMap, forkJoin } from 'rxjs';

@Component({
  selector: 'app-user-posts',
  imports: [],
  template: `
    <div class="page">
      @if (isLoading()) {
        <p>Loading...</p>
      } @else {
        <h2>{{ user()?.name }}'s Profile</h2>
        <p>{{ user()?.email }}</p>
        <h3>Posts ({{ posts().length }})</h3>
        @for (post of posts(); track post.id) {
          <div class="post">
            <strong>{{ post.title }}</strong>
            <p>{{ post.body }}</p>
          </div>
        }
      }
    </div>
  `,
  styles: [`
    .page { max-width: 700px; margin: 40px auto; padding: 0 24px; }
    h2 { font-size: 24px; color: #1a1a2e; margin-bottom: 4px; }
    p { color: #666; margin-bottom: 16px; }
    h3 { font-size: 18px; color: #1a1a2e; margin-bottom: 12px; }
    .post { background: white; padding: 16px; border-radius: 8px; margin-bottom: 12px; }
    .post strong { display: block; margin-bottom: 6px; color: #1a1a2e; }
  `]
})
export class UserPosts implements OnInit {

  private http = inject(HttpClient);

  user = signal<any>(null);
  posts = signal<any[]>([]);
  isLoading = signal(true);

  ngOnInit(): void {
    // Step 1: Get user 1
    this.http.get<any>('https://jsonplaceholder.typicode.com/users/1').pipe(
      switchMap(user => {
        // Step 2: Now that we have the user, use their ID to get their posts
        // We use forkJoin to also keep the user data
        return forkJoin({
          user: of(user),
          posts: this.http.get<any[]>(`https://jsonplaceholder.typicode.com/posts?userId=${user.id}`)
        });
      })
    ).subscribe({
      next: ({ user, posts }) => {
        this.user.set(user);
        this.posts.set(posts);
        this.isLoading.set(false);
      },
      error: () => this.isLoading.set(false)
    });
  }
}

// import of at the top!
import { of } from 'rxjs';

The switchMap takes the user response and uses user.id to construct the posts URL. The two requests happen sequentially — first the user is fetched, then (and only then) the posts are fetched using the user's ID.


Chapter 9 — Memory Leak Prevention — Unsubscription Strategies


9.1 — Why Memory Leaks Happen

A memory leak in Angular happens when a component is destroyed but a subscription is still active. The callback in .subscribe() keeps running, references to the component's properties are kept alive, and the component's memory is never freed.

This is especially bad in:

  • Subscriptions to interval or timer
  • Subscriptions to router events
  • Subscriptions to global services that never complete
  • WebSocket subscriptions

9.2 — Strategy 1 — Manual Unsubscribe

Store the subscription and call .unsubscribe() in ngOnDestroy:

export class MyComponent implements OnInit, OnDestroy {

  private subscription!: Subscription;

  ngOnInit(): void {
    this.subscription = someObservable$.subscribe(...);
  }

  ngOnDestroy(): void {
    this.subscription.unsubscribe();
  }
}

For multiple subscriptions, you can use a Subscription object to group them:

export class MyComponent implements OnInit, OnDestroy {

  private subscriptions = new Subscription();

  ngOnInit(): void {
    this.subscriptions.add(observable1$.subscribe(...));
    this.subscriptions.add(observable2$.subscribe(...));
    this.subscriptions.add(observable3$.subscribe(...));
  }

  ngOnDestroy(): void {
    this.subscriptions.unsubscribe();  // unsubscribes all at once
  }
}

9.3 — Strategy 2 — takeUntil with a destroy Subject

export class MyComponent implements OnInit, OnDestroy {

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

  ngOnInit(): void {
    observable1$.pipe(takeUntil(this.destroy$)).subscribe(...);
    observable2$.pipe(takeUntil(this.destroy$)).subscribe(...);
    observable3$.pipe(takeUntil(this.destroy$)).subscribe(...);
  }

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

One destroy$ Subject handles all subscriptions. Clean and scalable.


9.4 — Strategy 3 — async Pipe (Best Option When Possible)

The async pipe automatically unsubscribes when the component is destroyed. No ngOnDestroy needed:

export class MyComponent {
  data$ = this.service.getData();  // Observable
}
@if (data$ | async; as data) {
  <div>{{ data }}</div>
}

Use async pipe whenever you are using the Observable purely for display. It is the cleanest, most foolproof approach.


9.5 — Strategy 4 — take and takeUntilDestroyed

take(1) automatically completes after the first emission — perfect for one-time data loading where you know you only need one value:

this.http.get<User>('/api/user').pipe(
  take(1)  // complete after first response — automatically unsubscribes
).subscribe(user => this.user.set(user));

Angular also provides takeUntilDestroyed() from @angular/core/rxjs-interop which is the most modern approach:

import { Component, inject, OnInit } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { DestroyRef } from '@angular/core';

@Component({
  selector: 'app-example',
  imports: [],
  template: `<p>Count: {{ count }}</p>`
})
export class ExampleComponent implements OnInit {

  private destroyRef = inject(DestroyRef);

  count: number = 0;

  ngOnInit(): void {
    interval(1000).pipe(
      takeUntilDestroyed(this.destroyRef)
      // automatically unsubscribes when this component is destroyed
      // no ngOnDestroy needed
    ).subscribe(() => {
      this.count++;
    });
  }
}

takeUntilDestroyed is the cleanest modern approach. It requires no ngOnDestroy, no Subject, no manual cleanup. Just inject DestroyRef and pass it to takeUntilDestroyed.


Phase 9 — Complete Summary

Here is everything you learned in this phase.

Reactive programming — treating everything as streams of values over time. Solves problems that callbacks and Promises cannot — cancellation, multiple values, complex transformations.

Observable — a lazy blueprint for a stream. Nothing happens until .subscribe() is called. Can emit zero, one, or many values. Can be synchronous or asynchronous. Has three channels — next, error, complete.

Creation functionsof for static values, from for arrays and Promises, interval for repeating timers, timer for delayed emissions, fromEvent for DOM events, throwError for immediate errors.

Subscriptions — the return value of .subscribe(). Call .unsubscribe() to stop receiving values. Not unsubscribing from long-lived Observables causes memory leaks.

Cold vs Hot — cold Observables start fresh for each subscriber (HTTP calls). Hot Observables share one execution among all subscribers (Subjects, DOM events).

Operators — functions that transform Observables. Chained with .pipe(). Key operators: map transforms values, filter removes unwanted values, tap for side effects without changing the stream, take takes N values then completes, takeUntil completes when a notifier emits, debounceTime waits for silence before emitting, distinctUntilChanged skips duplicates, catchError handles errors, retry retries on failure, finalize runs on completion or error, startWith begins with a value.

The four map operatorsswitchMap cancels previous for search. mergeMap runs all in parallel for independent operations. concatMap queues for sequential operations. exhaustMap ignores new values while busy for form submission.

Subjects — both Observable and Observer. Subject for events. BehaviorSubject for state that needs a current value. ReplaySubject to replay recent history to new subscribers.

Combination operatorsforkJoin waits for all to complete (like Promise.all). combineLatest combines latest values when any changes. merge combines into one stream.

async pipe — subscribes in the template, auto-unsubscribes on destroy. Cleanest approach for display-only Observables.

Memory leak prevention — manual unsubscribe in ngOnDestroy, takeUntil with a destroy Subject, async pipe, take(1) for one-time loads, takeUntilDestroyed for modern clean approach.

Real patterns — search with debounceTime + distinctUntilChanged + switchMap, parallel loading with forkJoin, chaining API calls with switchMap, combining reactive values with combineLatest.


What's Next — Phase 10

In Phase 10 — Advanced Topics we cover everything that takes you from intermediate to professional Angular developer:

Angular Signals in complete depth — signal, computed, effect, input signals, model signals. Standalone component architecture best practices. Performance optimization — OnPush change detection, trackBy, deferrable views with @defer. Pipes — all built-in pipes and building custom pipes. Angular animations. Testing with Vitest — unit testing components, services, and pipes. Building for production and deployment.

No comments:

Post a Comment

Phase 4 - Fault Tolerance | Post 8 | Fault Tolerance — Keeping Your App Alive When Things Break

Post 8 of 15 | Phase 4: Fault Tolerance Fault Tolerance — Keeping Your App Alive When Things Break In every post so far we have assumed that...