AngularAtlased. 22.2

Ch. 14Advanced reactivity62 / 75

Advanced RxJS: higher-order operators

switchMap, mergeMap, concatMap and exhaustMap on a live diagram, plus retry with backoff, shareReplay and how to avoid leaks.

architecture ~0 min read Official docs

Signals have taken over the state, but events over time remain the territory of RxJS: keystrokes, clicks, overlapping requests, retries, websockets. The most important part, and the one most often got wrong, is the higher-order operators. Each value starts a new Observable, usually an HTTP request. The real question is: what do you do when the next value arrives while the previous request is still running?

There are four answers: cancel, run in parallel, queue, or ignore. Each has an operator. If you choose wrongly, the result is a race condition bug: old results overwriting the new ones, duplicate orders, saves applied in the wrong order.

Why it matters
“The search sometimes shows results for the previous text” or “the order was sent twice” are almost always a mergeMap where a switchMap or exhaustMap was needed. The difference does not show in quick tests, only on a slow network. You can reproduce exactly this in the demo below.

The problem: Observable of Observables

A map that returns a request produces an Observable<Observable<T>>: a stream of requests that nobody has subscribed to. The *Map operators subscribe to the inner streams themselves and flatten them into a single stream of results. The difference between them is only the overlap policy.

// "For each value, start an Observable" — the question is what you do when the next value arrives
// while the previous one is still running.
clicks$.pipe(map((id) => http.get(`/api/items/${id}`)));       // ✗ Observable<Observable<Item>>
clicks$.pipe(switchMap((id) => http.get(`/api/items/${id}`))); // ✓ Observable<Item> — plus a policy

The four operators

OperatorA new value arrives while the old one runsTypical example
switchMapcancels the old one (unsubscribe → HttpClient cancels the request)typeahead, route parameters
mergeMapruns them in parallel; the order of the results is not guaranteedindependent uploads
concatMapqueues it; runs one by one, in orderautosave, ordered commands
exhaustMapignores it until the current one finishessubmit, login
// Typeahead: only the last request matters; the old ones are CANCELLED (HttpClient also cancels the XHR/fetch)
readonly results = toSignal(
  this.query$.pipe(
    debounceTime(300),
    distinctUntilChanged(),
    switchMap((q) => this.api.search(q).pipe(catchError(() => of([])))),
  ),
  { initialValue: [] },
);
LiveLive marble diagram (real RxJS operators)

switchMap: a new click CANCELS the request in progress. Ideal for search (typeahead) and route parameters.

clicks (outer)
requests (inner)
result (output)
emitted: 0delivered: 0cancelled: 0
Choose an operator and press “Emit” several times, faster than the request takes. switchMap cuts the old bars (✕), mergeMap overlaps them, concatMap puts them one after another, exhaustMap ignores those made during a request.

Where to put catchError

An error completes the stream in which it occurs. If catchError sits after switchMap, the first failed request closes the whole stream: the search stops responding, with no error in the console. Put it inside the inner projection. Then only that request is replaced with a fallback value, and the outer stream carries on.

// ✗ catchError AFTER switchMap: the first error completes the outer stream — the search "dies"
query$.pipe(
  switchMap((q) => api.search(q)),
  catchError(() => of([])),
);

// ✓ catchError INSIDE the projection: only that request fails, the outer stream stays alive
query$.pipe(
  switchMap((q) => api.search(q).pipe(catchError(() => of([])))),
);

Retry with exponential backoff

retry({ count, delay }) resubscribes to the source after an error. The delay function receives the error and the retry number, and returns an Observable that says when to retry. Doubling the pause at each step (300, 600, 1200 ms…) does not hammer the server while it has problems. Rethrow the error for codes that will not fix themselves (401, 404). For HTTP, retry usually lives in an interceptor (see HttpClient & interceptors). For a connection that must survive server restarts — with jitter, so a thousand clients don’t come back in the same second — see WebSockets.

import { retry, timer } from 'rxjs';

http.get('/api/report').pipe(
  retry({
    count: 4,                                                // at most 4 retries
    delay: (error, retryCount) => {
      if (error.status === 401 || error.status === 404) throw error; // do not retry "definitive" errors
      return timer(300 * 2 ** (retryCount - 1));              // 300, 600, 1200, 2400 ms
    },
    resetOnSuccess: true,
  }),
);
Liveretry with backoff, on a source that fails
  1. Press the button.
Set how many failures the server has and how many retries you allow. The log shows the growing pauses. If the failures exceed count, you get the final error.

share and shareReplay

A “cold” Observable, such as http.get, runs on every subscribe. Three subscribers mean three requests. share() makes simultaneous subscribers share a single execution. shareReplay also keeps the last value for those who arrive later, so it works like a cache. The trap is a plain shareReplay(1): it has refCount: false, so it never unsubscribes from the source. On a stream that never ends (a websocket, an interval), the source keeps running after all the subscribers have left.

A subtle detail with refCount: true: the cache is kept only if the source managed to finish. If all subscribers leave before complete (for example firstValueFrom, take(1), or a component destroyed during the request), shareReplay unsubscribes from the source and starts over with the next subscriber.

// Without sharing: each subscribe = one more execution (one more request)
const user$ = http.get<User>('/api/me');
user$.subscribe(); user$.subscribe();                 // 2 requests

// share(): SIMULTANEOUS subscribers share one execution; after complete it resets
const shared$ = http.get<User>('/api/me').pipe(share());

// shareReplay: keeps the last value for LATE subscribers (cache)
const cached$ = http.get<User>('/api/me').pipe(shareReplay({ bufferSize: 1, refCount: true }));
// refCount: true → when the last subscriber leaves, it also unsubscribes from the source.
// shareReplay(1) (refCount: false) on a source that never completes = the source runs forever.
LiveHow many executions? And who stays subscribed?
3 simultaneous subscribers + 1 late subscriber to a “request” of 400 ms; then an infinite stream, from which all of them unsubscribe.
Variant Request executions Late subscriber Infinite stream after unsubscribing
Press “Run the scenario” (takes ~2 s).
The scenario runs with real RxJS: the same 3 simultaneous subscribers and 1 late one, then an infinite stream that everyone unsubscribes from. Only shareReplay(1) leaves the source running. The demo sources are stopped at the end.

Combining streams

combineLatest emits whenever any source changes, once each has emitted at least once. It suits combined filters. withLatestFrom emits only from the main source and takes the current value from the others (“on click, save the current form”). forkJoin waits for all of them to complete and emits once: it is a Promise.all for requests.

// combineLatest: emits on every change, after ALL of them have emitted once — combined filters
combineLatest([category$, sort$]).pipe(switchMap(([c, s]) => api.list(c, s)));

// withLatestFrom: emits only from the main source, taking the latest value from the others
save$.pipe(withLatestFrom(form$), concatMap(([, value]) => api.save(value)));

// forkJoin: waits for ALL of them to complete, a single emission — independent parallel requests
forkJoin({ user: api.me(), settings: api.settings() });

// zip: pairs the n-th value of each — rarely useful in a UI

No forgotten subscriptions

Every manual subscribe in a component must be closed. takeUntilDestroyed() does it automatically when the component is destroyed. For values shown in the template, toSignal() is even simpler: it subscribes and unsubscribes by itself (see Signals ↔ RxJS).

import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

export class Live {
  constructor() {
    interval(1000)
      .pipe(takeUntilDestroyed()) // in an injection context; otherwise: takeUntilDestroyed(this.destroyRef)
      .subscribe((n) => this.tick.set(n));
  }
}
// For state shown in the template, toSignal(obs$) unsubscribes by itself (see the Signals ↔ RxJS page).

When YES, when NO

RxJS is the right tool
  • Events over time: debounce, throttle, time windows, combining several sources.
  • Requests that can overlap and need a policy: cancellation, queueing, ignoring.
  • Retries, timeouts, polling, websockets, server-sent events.
  • Ordered command flows (autosave, offline queues).
Signals / resource are enough
  • State shown in the UI: signal / computed (see Signals).
  • A request that depends on parameters: httpResource / rxResource cancel the outdated one by themselves (see resource).
  • A single request per click, with no possible overlap: a plain subscribe or an async method.
  • “To make it reactive”: a BehaviorSubject for a simple value is now a signal().

Common pitfalls

  • mergeMap for search. Responses arrive in any order, and an old one can overwrite the new one. Use switchMap.
  • switchMap for saves. A save cancelled halfway may already have been processed by the server, or may not. For writes, use concatMap or exhaustMap. More on writes that arrive out of order in optimistic updates.
  • Subscribe inside subscribe. You lose cancellation and error handling. Any “subscribe, then another subscribe in the callback” is a *Map operator.
  • catchError at the outer level of a stream that must stay alive: it stops everything at the first error.
  • shareReplay(1) on infinite sources: memory and connection leaks. Use { bufferSize: 1, refCount: true }. How to find them is on the memory leaks page.
  • Retry on 4xx: you retry a request forever that will never succeed. Filter by status.

Cheat sheet

OPERATOR      WHEN A NEW VALUE ARRIVES WHILE THE OLD ONE RUNS  USE FOR
switchMap     cancel the old one                               search, route parameters, "the last one wins"
mergeMap      run them in parallel (optional: concurrency)     independent operations, uploads
concatMap     queue it, in order                               ordered saves, command queues
exhaustMap    ignore it                                        submit, login, manual refresh

catchError    INSIDE the inner projection, so the outer stream stays alive
retry         { count, delay: (err, n) => timer(base * 2 ** (n - 1)) } — don't retry 4xx
share()       simultaneous subscribers share the execution
shareReplay   { bufferSize: 1, refCount: true } — cache + correct unsubscription
combineLatest combined filters · withLatestFrom "on click, take the current value" · forkJoin "all of them, once"
takeUntilDestroyed() / toSignal()   no forgotten subscriptions

Ch. 14 · 0 of 4 complete

Finished this lesson?

Mark it and it shows up on your map: in the contents and on the home page.

Marks itself as done once you have read the whole lesson.

  1. Signals under the hood
  2. Advanced RxJS: higher-order operators
  3. NgRx SignalStore
  4. State machines with signals
See your whole map

All 75 pages · type to filter

  • 01What's new in Angular 21 & 22All the important changes in v21 and v22, briefly, with a link to the page that explains each one.Start
  • 01Your first app, step by stepA reading list built from an empty component to a finished app — signals, @for, inputs and outputs, a service and a form, one step at a time.Start
  • 01Quick decisions“Where do I put this?”, “Signal or Observable?”, “Shared or feature?” — short answers to everyday questions.Start
  • 01Angular interview questions60 real interview questions for junior, mid and senior, with the answer to give, the follow-up and the red flags. Plus a mock interview.Start
  • 02Standalone & bootstrapHow an app starts without NgModule: bootstrapApplication, app.config.ts, imports in the component.Modern fundamentals
  • 02Data binding & eventsInterpolation, property and attribute bindings, events with $event and key filters, and how [( )] is built — what goes into the DOM and what comes back.Modern fundamentals
  • 02Control flow: @if, @for, @switchThe block syntax that replaces *ngIf/*ngFor/ngSwitch, with track, @empty and exhaustive checks.Modern fundamentals
  • 02@let and the new template expressionsLocal variables with @let, arrow functions, spread, regex, instanceof, comments inside tags.Modern fundamentals
  • 02@defer — lazy loading in templatesLoad heavy components only when needed: triggers, prefetch, placeholder, loading, error.Modern fundamentals
  • 02Class & style bindingsWhy [class.x] and [style.x] replaced ngClass and ngStyle (with automatic migration from v21).Modern fundamentals
  • 02Component styles & encapsulation:host, the three encapsulation modes, theming with CSS variables, and why ::ng-deep is a last resort.Modern fundamentals
  • 02Pipes: when and howPure vs impure pipes, custom pipes, and how you choose between a pipe, computed() and a method.Modern fundamentals
  • 03signal() and computed()The reactive primitives: writable state, derived state, equality, untracked and immutability rules.Signals & reactivity
  • 03effect() — when YES, when NOSide effects synchronised with signals: localStorage, logging, imperative APIs. And their traps.Signals & reactivity
  • 03linkedSignal() — editable derived stateA signal that resets when its source changes, but that you can also set manually.Signals & reactivity
  • 03resource, rxResource, httpResourceAsync data as signals: status, value, error, reload, automatic cancellation. Stable since v22.Signals & reactivity
  • 03debounced() — delayed signalsThe “debounce” version of a signal, without RxJS. Perfect for search.Signals & reactivity
  • 03Lists: search, filter, sort, paginateA chain of computed signals that recalculates only what changed, with the filters kept in the URL so a link reopens the same view.Signals & reactivity
  • 03Observables from zeroWhat an Observable is, subscribe and unsubscribe, the operators you use every week, cold vs hot — and when a signal is enough.Signals & reactivity
  • 03Signals ↔ RxJStoSignal, toObservable, takeUntilDestroyed, outputFromObservable — and when RxJS remains the right choice.Signals & reactivity
  • 04input() and output()Inputs as signals (required, transform, alias) and typed outputs. Data down, events up.Components: communication
  • 04model() — two-way bindingBanana-in-a-box [( )] with a single API. When to use it, and when input + output is better.Components: communication
  • 04viewChild & contentChildQueries as signals: viewChild, viewChildren, contentChild, contentChildren, the read option.Components: communication
  • 04Lifecycle & DestroyRefWhich hooks still matter in the signals era, afterNextRender, DestroyRef and typed ngOnChanges.Components: communication
  • 04Content projectionng-content with select, multiple slots, default content and the trap of conditional projection.Components: communication
  • 04ng-template & ngTemplateOutletTemplates as values: render props in Angular, typed context and customising components.Components: communication
  • 05Smart vs DumbContainer vs presentational: who holds the state, who only displays, and how to split them properly.Component design
  • 05Generic components <T>A correctly typed DataTable<T> or Select<T>, with templates for cells.Component design
  • 05Dynamic componentsNgComponentOutlet, createComponent with inputBinding/outputBinding, and a dashboard built from config.Component design
  • 05Directives & host compositionAttribute directives, the host: {} metadata and the Directive Composition API (hostDirectives).Component design
  • 05Custom form controlsFormValueControl (Signal Forms) vs ControlValueAccessor: a star rating, in both variants.Component design
  • 05Change detectionOnPush is the default from v22, zone.js is gone from v21. What triggers rendering and how not to get burned.Component design
  • 06inject() & @Service()The new @Service() decorator from v22, inject() vs the constructor, and the injection context.Dependency Injection
  • 06Providers & the injector hierarchyRoot vs route vs component, useClass/useValue/useFactory/useExisting, and the optional/self/skipSelf modifiers.Dependency Injection
  • 06InjectionToken & provideX()Typed configuration, provideFeature() functions, makeEnvironmentProviders and initializers.Dependency Injection
  • 06injectAsync() — lazy servicesLoad a heavy service only when you need it, with prefetch on idle.Dependency Injection
  • 07Folder structurecore / shared / features, what goes into each, and what a feature looks like inside.Application architecture
  • 07Shared vs featureWhen to move a component into shared and (especially) when NOT to. A concrete checklist.Application architecture
  • 07Interfaces & modelsInline interface or separate file? DTO vs domain model, type vs interface, where to put them.Application architecture
  • 07State management with signalsSimple signal stores, facades, local vs global state, and when you need NgRx.Application architecture
  • 07Style guide & namingThe current official conventions: file names without suffixes, protected, readonly, member order.Application architecture
  • 07Barrels, aliases & boundariesindex.ts, path aliases, a feature’s public API and dependency rules that can be checked automatically.Application architecture
  • 07Errors: @boundary & ErrorHandlerThe new @boundary block from v22.2, the global ErrorHandler, HTTP errors and errors from resource.Application architecture
  • 08Modern routing & lazy loadingloadComponent, child routes, parameters as inputs and parameter inheritance (the new default in v22).Routing
  • 08Guards & resolverscanActivate, canMatch, canDeactivate and resolve as functions, with redirects via UrlTree / RedirectCommand.Routing
  • 09Signal Forms — the basicsForms built on signals: form(), [formField], schema-based validation. Stable since v22.Forms
  • 09Signal Forms — advancedCustom and cross-field validation, async/HTTP with debounce, dynamic lists, disabled/hidden, reusable schemas.Forms
  • 09Typed Reactive FormsTyped FormGroup, nonNullable, FormArray — and how they coexist with Signal Forms.Forms
  • 10HttpClient, interceptors & the API layerHttpClient without setup (v21), Fetch by default (v22), functional interceptors and an API layer with DTO mapping.HTTP & data
  • 11Performance — a checklistWhat really matters: OnPush + signals, track, @defer, lazy routes, images, bundles and hydration.Quality & performance
  • 11Testing with VitestVitest is the default runner from v21. TestBed, components with signals, services, HTTP and harnesses.Quality & performance
  • 11DevTools: look inside your appChrome DevTools and Angular DevTools step by step: what each panel shows, with exercises that work on this site.Quality & performance
  • 11Angular AriaHeadless, accessible components that you style however you like: tabs, accordion, menu, listbox...Quality & performance
  • 12SSR, prerender & render modesWhen to render on the server, when at build time and when only in the browser: RenderMode per route, app.routes.server.ts and the real costs.SSR & hydration
  • 12Hydration: event replay & incrementalHow the browser takes over server HTML without re-rendering it: full hydration, event replay, @defer (hydrate on …) and mismatch errors.SSR & hydration
  • 12Server-compatible codewindow doesn’t exist on the server: PLATFORM_ID, afterNextRender, DOCUMENT, TransferState, the HTTP cache and PendingTasks.SSR & hydration
  • 13Micro frontendsWhen it is worth splitting an app into several deployments and when it is just complexity: Native Federation, Module Federation, iframes, Web Components.Architecture at scale
  • 13Web Components with Angular ElementscreateCustomElement: an Angular component used as an HTML tag anywhere, plus communication between apps through events and BroadcastChannel.Architecture at scale
  • 13Monorepo, libraries & boundariesA workspace with several apps and libraries: ng generate library, Nx, secondary entry points, publishing and dependency rules.Architecture at scale
  • 13Hexagonal architecture (ports & adapters)A domain without Angular, ports as abstract classes and interchangeable adapters: HTTP, localStorage or in-memory, without touching the UI.Architecture at scale
  • 14Signals under the hoodThe reactive graph: producers and consumers, push-pull, why computed is lazy and glitch-free, the diamond problem and when effects run.Advanced reactivity
  • 14Advanced RxJS: higher-order operatorsswitchMap, mergeMap, concatMap and exhaustMap on a live diagram, plus retry with backoff, shareReplay and how to avoid leaks.Advanced reactivity
  • 14NgRx SignalStoreLarge-scale state management on signals: withState, withComputed, withMethods, rxMethod, entities and hooks — and when a simple store is enough.Advanced reactivity
  • 14State machines with signalsNo more impossible states: a typed machine on signals, with transitions, guards and a live state diagram.Advanced reactivity
  • 15WebSockets: real time that survives the networkA typed socket as signals: reconnect with backoff and jitter, heartbeat, an outbox while offline, and SSR safety.Real time & offline
  • 15Optimistic updates & rollbackAn instant UI that stays honest: snapshot and rollback, out-of-order responses, idempotency keys.Real time & offline
  • 15Offline-first: Service Worker & cachingThe Angular Service Worker: caching strategies, update prompts and an outbox for changes made offline.Real time & offline
  • 16Web WorkersHeavy computation without blocking the UI: a real worker in the page, message passing and when it is not worth it.Advanced UI & performance
  • 16Huge lists: virtual scroll100,000 rows without lag: CDK virtual scroll, itemSize, trackBy and the alternatives (pagination, infinite scroll).Advanced UI & performance
  • 16Modern animations & View Transitionsanimate.enter and animate.leave instead of the deprecated @angular/animations, CSS transitions and View Transitions on navigation.Advanced UI & performance
  • 16Memory leaks: finding and fixing themSubscriptions, timers and listeners that outlive their component — how to spot them in DevTools and close them.Advanced UI & performance
  • 17Security: XSS, sanitisation & CSPWhat Angular does automatically against XSS, when bypassSecurityTrust becomes dangerous, plus CSP with nonce, Trusted Types and XSRF.Security & globalisation
  • 17Authentication: tokens & refreshAn interceptor that attaches the token, refreshes once on 401 and replays the queued requests — plus where you keep the token.Security & globalisation
  • 17OAuth 2.1 & OpenID Connect with PKCEThe authorization code flow with PKCE, step by step: verifier and challenge, state and nonce, the ID token, and why a BFF is safer.Security & globalisation
  • 17Internationalisation (i18n)$localize, the i18n attribute, ICU plural/select, a build per language vs runtime translations, and locale-based formatting.Security & globalisation