The Essential Guide to RxJS Operators in Angular

Babatunde Lamidi
7 min readJun 10, 2024

--

In modern web development, handling asynchronous data and events efficiently is crucial. The Angular framework leverages RxJS (Reactive Extensions for Javascript) to manage and manipulate streams of data. RxJS provides a powerful way to work with asynchronous operations through observables and a wide range of operators. This guide will delve into the most commonly used RxJS operators in Angular, explain how they, and illustrate their usage with easy-to-understand examples.

Before we get down into the nitty-gritty of the article, let me throw more light on what an Observable is:

Observable: An observable is a data producer in the RxJS library, which emits values over time. Observables are a key part of reactive programming, allowing you to handle asynchronous data streams in a predictable manner. When you subscribe to an Observable, you are notified whenever it emits a new value, encounters an error, or completes.

In simpler terms, an Observable is like my YouTube channel, which contains lots of videos. When someone subscribes to my channel, they get notified anytime I drop a new video.

  • YouTube Channel (Observable): This is the source of the content (videos).
  • Videos (Data): These are the individual pieces of content that get produced over time.
  • Subscribing to the Channel (Subscribing to the Observable): When someone subscribes to my YouTube channel, they want to receive notifications whenever a new video is uploaded.
  • Notifications (Emissions): These are the alerts or updates that subscribers receive whenever there’s a new video.

YouTube Analogy code example:

import { Observable } from 'rxjs';

const youtubeChannel$ = new Observable(subscriber => {
subscriber.next('New Video 1');
setTimeout(() => {
subscriber.next('New Video 2');
}, 1000);
setTimeout(() => {
subscriber.complete();
}, 2000);
});

// Subscribe to the Observable
youtubeChannel$.subscribe({
next: video => console.log(`Notification: ${video}`),
complete: () => console.log('No more new videos!')
});

// Output:
// Notification: New Video 1
// Notification: New Video 2
// No more new videos!

What is RxJS ?

RxJS is a library for reactive programming using Observables, to make it easier to compose asynchronous or callback-based code. Observables are streams of data that you can listen to and react to. RxJS comes with a wide range of operators that allow you to manipulate these streams in various ways.

The Basics: The pipe Function

Before we dive into the operators, it’s essential to understand the pipe function in RxJS. Think of pipe as a magic tube through which data flows and gets transformed along the way. Each transformation is performed by an operator.

For example, if you have a stream of numbers and you want to double each number and then only keep the even ones, you would use the pipe function to connect these transformations:

import { from } from 'rxjs';
import { map, filter } from 'rxjs/operators';

const numbers$ = from([1, 2, 3, 4, 5]);

const transformedNumbers$ = numbers$.pipe(
map(num => num * 2),
filter(num => num % 2 === 0)
);

transformedNumbers$.subscribe(console.log); // Output: 2, 4, 6, 8, 10

// map and filter are operators that transform the stream of numbers (stream of data).

Top 10 Most Used RxJS Operators in Angular

Let’s explore the top 10 most used RxJS operators in Angular, explaining each with simple illustrations, for more clarity I will be using the example of a lemon/lemonade to explain these RxJS operators.

map

Transforms each item emitted by an Observable by applying a function to each item.

Imagine you have a bunch of lemons. The map tool is like your juicer that turns each lemon into lemonade. It transforms one thing (lemons) into another (lemonade).

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

from([1, 2, 3]).pipe(
map(x => x * 2)
).subscribe(console.log); // Output: 2, 4, 6


from(['lemon1', 'lemon2', 'lemon3']).pipe(
map(lemon => `lemonade from ${lemon}`)
).subscribe(console.log); // Output: lemonade from lemon1, lemonade from lemon2, lemonade from lemon3

filter

Emits only the items from an Observable that meet a certain condition.

You have a pile of lemons, but you only want the ripe ones for your lemonade. The filter tool helps you pick out just the ripe lemons and ignore the unripe ones.

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

from([1, 2, 3, 4, 5]).pipe(
filter(x => x % 2 === 0)
).subscribe(console.log); // Output: 2, 4

// The filter operator keeps only even numbers.

from(['ripe lemon', 'unripe lemon', 'ripe lemon']).pipe(
filter(lemon => lemon.startsWith('ripe'))
).subscribe(console.log); // Output: ripe lemon, ripe lemon

switchMap

Projects each source value to an Observable which is merged in the output Observable, emitting values only from the most recently projected Observable.

You are squeezing lemons into a pitcher of water. If you get a better, juicier lemon, you immediately throw away the old one and start using the new one. switchMap makes sure you’re always using the best, most recent lemon.

import { fromEvent, interval } from 'rxjs';
import { switchMap } from 'rxjs/operators';

fromEvent(document, 'click').pipe(
switchMap(() => interval(1000))
).subscribe(console.log); // Output: 0, 1, 2, ... on each click

// Each click event starts a new interval, canceling the previous one.

fromEvent(document, 'click').pipe(
switchMap(() => of('best lemon'))
).subscribe(console.log); // Output: best lemon (on each click)

mergeMap

Projects each source value to an Observable which is merged into the output Observable.

You have multiple lemon squeezers, and you want to use all of them to make lemonade. mergeMap lets you use all the squeezers at the same time, one after another, to get as much juice as possible without waiting.

import { fromEvent, interval } from 'rxjs';
import { mergeMap } from 'rxjs/operators';

fromEvent(document, 'click').pipe(
mergeMap(() => interval(1000))
).subscribe(console.log); // Output: 0, 0, 1, 1, 2, 2, ...

// All intervals from each click run concurrently.

fromEvent(document, 'click').pipe(
mergeMap(() => of('juice from squeezer'))
).subscribe(console.log); // Output: juice from squeezer (for each click)

concatMap

Projects each source value to an Observable which is concatenated in the output Observable.

You have a line of customers each with their lemons to squeeze. concatMap lets you squeeze lemons one customer at a time, in order, making sure you don’t skip anyone.

import { fromEvent, interval } from 'rxjs';
import { concatMap } from 'rxjs/operators';

fromEvent(document, 'click').pipe(
concatMap(() => interval(1000).pipe(take(3)))
).subscribe(console.log); // Output: 0, 1, 2, 0, 1, 2, ...
// Intervals run one after another, in order.

fromEvent(document, 'click').pipe(
concatMap(() => of('lemon from customer').pipe(take(3)))
).subscribe(console.log);

catchError

Catches errors on the observable to be handled with another observable or function.

If you accidentally squeeze a bad lemon and it ruins the lemonade, catchError catches the bad juice and gives you a new, fresh lemon so you can continue making lemonade without stopping.

import { of, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

throwError('Error!').pipe(
catchError(error => of('Fallback value'))
).subscribe(console.log); // Output: Fallback value
//If an error occurs, a fallback value is provided.

throwError('bad lemon').pipe(
catchError(() => of('new lemon'))
).subscribe(console.log); // Output: new lemon

tap

Performs side effects for notifications from the source Observable.

As you squeeze each lemon, you make a note of how juicy it is without changing anything about the process. tap is like keeping a log of what you’re doing while making the lemonade.

import { from } from 'rxjs';
import { tap } from 'rxjs/operators';

from([1, 2, 3]).pipe(
tap(x => console.log(`Value: ${x}`))
).subscribe();
// Logs each value without altering the stream.

from(['lemon1', 'lemon2', 'lemon3']).pipe(
tap(lemon => console.log(`Squeezing ${lemon}`))
).subscribe();

debounceTime

Emits a value from the source Observable only after a particular time span has passed without another source emission.

If customers keep asking for lemonade too quickly, it gets overwhelming. debounceTime makes sure there’s a pause between each customer’s request, so you have enough time to make the best lemonade without rushing.

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

fromEvent(document, 'click').pipe(
debounceTime(300)
).subscribe(console.log);
// Waits for 300ms after the last click before emitting.

fromEvent(document, 'click').pipe(
debounceTime(300)
).subscribe(console.log); // Output: click event after 300ms

take

Emits only the first n values emitted by the source Observable.

You have a huge basket of lemons, but you only need a few to make enough lemonade for your stand. take lets you choose just the first few lemons and ignore the rest.

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

from([1, 2, 3, 4, 5]).pipe(
take(3)
).subscribe(console.log); // Output: 1, 2, 3
// Takes only the first three values.

from(['lemon1', 'lemon2', 'lemon3', 'lemon4']).pipe(
take(2)
).subscribe(console.log); // Output: lemon1, lemon2

combineLatest

Combines multiple Observables to create an Observable whose values are calculated from the latest values of each input Observable.

You and a friend are running the lemonade stand together. You are squeezing lemons while your friend is adding sugar. combineLatest takes the most recent lemonade and the most recent sugar and combines them to make the perfect glass of lemonade.

import { of, interval } from 'rxjs';
import { combineLatest } from 'rxjs/operators';

const streamA$ = interval(1000).pipe(take(3)); // 0, 1, 2
const streamB$ = of('a', 'b', 'c');

combineLatest([streamA$, streamB$]).subscribe(console.log); // Output: [0, 'a'], [1, 'b'], [2, 'c']
// Combines the latest values from multiple streams.

const lemons$ = of('lemonade1', 'lemonade2');
const sugar$ = of('sugar1', 'sugar2');

combineLatest([lemons$, sugar$]).subscribe(console.log); // Output: [lemonade1, sugar1], [lemonade2, sugar2]

In real applications you can combine many operators to handle complex data streams. Below is a more complex example combining even more operators:

import { fromEvent, interval, of } from 'rxjs';
import { map, filter, switchMap, catchError, tap, debounceTime, take, combineLatest } from 'rxjs/operators';

// Simulated API call that may fail
const apiCall$ = of('data').pipe(
tap(() => {
if (Math.random() < 0.5) throw new Error('API error');
}),
catchError(error => of('fallback data'))
);

const clicks$ = fromEvent(document.getElementById('myButton'), 'click');

const latestData$ = clicks$.pipe(
switchMap(() => apiCall$)
);


const combined$ = clicks$.pipe(
switchMap(() => interval(1000).pipe(
take(5), // Only take the first 5 numbers
map(num => num * 2), // Double each number
filter(num => num % 2 === 0), // Only keep even numbers
debounceTime(300), // Wait for 300ms silence between numbers
combineLatest(latestData$), // Combine with the latest data from API call
map(([num, data]) => `${num} with ${data}`) // Combine the number and data
))
);

combined$.subscribe(result => console.log(result));

In this guide, we explored the essential RxJS operators in Angular, understanding their functionalities through relatable examples. By now, you should have a solid grasp of how operators like map, filter, switchMap, mergeMap, concatMap, catchError, tap, debounceTime, take, and combineLatest work and how they can be applied in your Angular applications.

--

--

Babatunde Lamidi

Frontend Engineer lost in the matrix of Frontend engineering and Poetry