siliceum

Angular and RxJS: Reactivity yes... but controlled!

When I discovered RxJS a few years ago with Angular, I just saw .subscribe() everywhere on things called observables.

But what I didn’t see yet were memory leaks, weird behaviors, and wobbly operator chains.

Over time, I understood that reactive programming is much more than a tool for managing API calls. It’s a paradigm shift, which also implies good practices and a different mental model.

But first, what is an Observable?

An observable is an asynchronous data stream that can be listened to over time.

It’s at the heart of reactive programming with RxJS.

The basic idea: a source that emits values

Imagine a pipe through which data arrives over time:

const obs$ = new Observable(observer => {
  observer.next(1);
  observer.next(2);
  observer.next(3);
  observer.complete();
});

Here, we create a stream that emits 1, 2, 3, then stops. To “listen” to this stream, we use subscribe():

obs$.subscribe(value => console.log(value));
// outputs: 1, 2, 3

Difference with a Promise

FeaturePromiseObservable
ValuesSingleMultiple over time
CancelableNoYes
Lazy (on demand)YesYes
Operators.then(), .catch().pipe(), map(), filter()
ExecutionUpon creationAt subscribe() time
searchInput$.pipe(
  debounceTime(300),
  switchMap(query => this.api.search(query))
).subscribe(results => {
  this.results = results;
});
  • searchInput$ is an observable of keyboard events: we get changes from the “Search for a user” field
  • debounceTime(300): waits for a 300ms pause: we want to avoid launching a search every time a user presses a key. The debounce here waits 300ms after the last emitted value to continue the stream
  • switchMap(...): we switch observables in the stream: this.api.search(query) will instantiate a new observable (an http request) that we really want to wait for to get its result. So we switch streams
  • subscribe(...): receives results and updates the UI (actually, if you’re doing Angular, I prefer the | async pipe)

An observable can:

  • emit 0, 1 or multiple values
  • be infinite (for example: fromEvent, interval)
  • be cold or hot (see next section)
  • be combined, transformed, filtered, delayed, retried, etc.

RxJS provides over 60 operators to compose streams in an expressive and readable way.

Cold vs Hot Observable: understanding the difference

Cold Observable

An observable is said to be “cold” when the source is re-evaluated for each subscription. This means each subscriber has its own lifecycle.

const cold$ = new Observable(observer => {
  console.log('New subscriber');
  observer.next(Math.random());
});

cold$.subscribe(val => console.log('Subscriber 1:', val));
cold$.subscribe(val => console.log('Subscriber 2:', val));
New subscriber
Subscriber 1: 0.42
New subscriber
Subscriber 2: 0.88

Typically: http.get(), of(), from()

Hot Observable

An observable is said to be “hot” when it shares its data source between subscribers.

const subject = new Subject();

subject.subscribe(val => console.log('Subscriber 1:', val));
subject.next(1);
subject.next(2);

subject.subscribe(val => console.log('Subscriber 2:', val));
subject.next(3);
Subscriber 1: 1
Subscriber 1: 2
Subscriber 1: 3
Subscriber 2: 3

Typically: Subject, fromEvent, WebSocket, etc.

Cold + sharing = hot (with shareReplay)

It’s possible to make a cold observable “hot” by sharing it with share() or shareReplay():

const api$ = this.http.get('/data').pipe(shareReplay(1));

This avoids triggering multiple HTTP requests if multiple subscribers subscribe.

Best practices to adopt

With great power comes great responsibility, as the uncle would say.

1. Always handle unsubscription

A subscribe() without unsubscribe() = guaranteed memory leak. By default, subscriptions to observables don’t trigger automatic unsubscription (like JS events actually).

What to do:

  • Use takeUntil() or take(1): the operator completes the observable automatically

    • takeUntil(notifier$)

    It works by listening to another observable notifier$ which, when it emits, causes completion (and thus unsubscription) of the source observable.

    It doesn’t trigger unsubscribe until this notifier emits, but once done, unsubscription is automatic.

    • take(1) automatically completes the observable after the first emission, which triggers an automatic unsubscribe for this subscription.
  • Unsubscribe unsubscribe() in your component’s ngOnDestroy

  • Use async pipe in Angular templates: The async pipe handles everything: it subscribes AND unsubscribes automatically.

<div *ngIf="data$ | async as data">
  {{ data.title }}
</div>

2. Avoid nested subscriptions

Don’t do this:

this.a$.subscribe(a => {
  this.b$.subscribe(b => {
    // ...
  });
});

Use switchMap, mergeMap, concatMap, depending on the desired behavior:

this.result$ = this.a$.pipe(
  switchMap(a => this.b$)
);

Why?

  • Because you need three boxes of Aspirin to read a pyramid of doom (you know, when you have a bunch of nested subscribe()
  • Error handling becomes complicated: each subscribe() manages its own errors. Nesting = guaranteed acrobatics to propagate or centralize errors correctly
  • Guaranteed memory leaks
  • Uncontrolled execution order

3. Choose the right operator

There’s a whole bunch of them, so don’t hesitate to pick the most relevant one! Some I use the most:

  • switchMap(): cancels the previous observable on each new emission

    Useful for real-time user searches

  • debounceTime(500): waits for a calm period before emitting

    Useful for reducing noise on search fields

  • map(val => val * 2): transforms values

    Simple data manipulation

  • throttleTime(2000): ignores emissions too close together

    Anti-spam on submit button

  • shareReplay(1): shares and caches the latest values

    To avoid calling an API 5 times for 5 subscribers

Link to go further: learnrxjs.io

Think long-term:

RxJS is an extremely powerful tool, but without good practices, it can quickly become hard to manage.

Before diving into code, take time to think about:

  • The “temperature” of your observables: clearly distinguish “cold” observables (which produce data on each subscription) from “hot” ones (which emit independently of subscribers).
  • Who subscribes, when and why: understand the lifecycle of your subscriptions to avoid memory leaks.
  • How to handle cancellation: always plan how and when your subscriptions should be cleaned up (unsubscribed).
  • The robustness of your streams: imagine your operator chains capable of handling errors, interruptions, and even evolving without breaking the application (for example with logical rollbacks).

Finally, test your RxJS streams independently from components to guarantee their reliability and ease maintenance.

In summary

RxJS is not just a tool for “making API calls” or “listening to events”.

It’s a structured and declarative way of thinking about asynchronous data stream management.

And like any powerful tool misused… it can become a nightmare.

And you, what are your favorite RxJS operators?

Photo de Mickaël  JACQUOT

Written by

Mickaël JACQUOT

FullStack Developer