Skip to main content

RxJS

This article contains common patterns and best practices for using RxJS.

Reactive programming is a common programming pattern, and deeply embedded in the Angular framework.

Using operators

RxJS is a powerful library with many built-in operators to manipulate your streams. If you need help finding the RxJs operator you need for your use case, use this tool: RxJS operator decision tree.

Subscription management

RxJS follows the observer pattern: we can subscribe certain objects, the observers, to another object, called the observable. Whenever an event occurs, the observable notifies all its observers. An observer lives as long as you tell it to stop listening for changes. This does not happen automatically in many cases, so you have to be conscientious about it.

Use the async pipe

Avoid manually subscribing and unsubscribing Observables. Use the async pipe provided by Angular as much as possible. This saves boilerplate code and unsubscribes automatically. Read more about how to use it in the docs.

Always unsubscribe

Don’t forget to unsubscribe. Whenever you subscribe, triple-check if you need to unsubscribe. Unsubscribing can be done in many ways. The most obvious one is calling the .unsubscribe() method on your subscription.

Always unsubscribe explicitly

To prevent forgetting to unsubscribe, we recommend always unsubscribing explicitly, mostly in your component ngOnDestroy life-cycle hook. If you see .subscribe(), you should also see .unsubscribe() somewhere in your code.

Unsubscribe on component destroy

Many times you want to subscribe to an observable, and unsubscribe when the component is destroyed. You can manually unsubscribe in the ngOnDestroy lifecycle hook:

#someSubscription: Subscription;

ngOnDestroy() {
this.#someSubscription.unsubscribe();
}

Unsubscribe using operators

You can also unsubscribe using operators, mostly using the take* operators provided by RxJS.

// Unsubscribe when another observable emits a value
observable$.pipe(takeUntil(otherObservable$)).subscribe();

// Unsubscribe after receiving one value
observable$.pipe(take(1)).subscribe();
Make sure the operator triggers

Note that a subscription will not be unsubscribed if the take* operator does not trigger before the component is destroyed.

Unsubscribe multiple subscriptions

A common pattern to unsubscribe many subscriptions that live inside your component is as follows:

// Create a new subscription instance
const subscription = new Subscription();

// Subscribe to some streams
subscription.add(first$.subscribe(/* handle value */));
subscription.add(second$.subscribe(/* handle value */));

// Unsubscribe all collected subscription at once
subscription.unsubscribe();

Don’t put logic inside your subscribe functions

Don’t put any logic inside your subscribe function. Create a separate function or method to handle the results of your stream. This will increase testability and legibility.

🚫 Avoid:

class MyComponent {
// ...

constructor() {
movies$.subscribe((movies) => {
this.loading = false;

if (movies.length === 0) {
return;
}

this.movies = movies;
});
}
}

✅ Better:

// Somewhere in your class
class MyComponent {
constructor() {
// Subscribe
movies$.subscribe((movies) => this.handleMovies(movies));

// Or, even shorter:
movies$.subscribe(this.handleMovies);
}

#handleMovies(movies: Array<Movie>) {
this.loading = false;

if (movies.length === 0) {
return;
}

this.movies = movies;
}
}

Don’t use nested subscriptions

Never nest subscriptions. Nesting makes our code difficult to test, complex and can cause bugs. Use pipes if you need to transform data or trigger behavior based on values passed through a stream.

🚫 Avoid:

params
.pipe(map(params => params.id))
.subscribe(id =>
this.userService.fetchById(id).subscribe(
user => this.preferenceService.fetchForUser(user.uuid).subscribe(
preferences => {
this.userPreferences = preferences;
}
)
);
);

✅ Better:

params
.pipe(
map((params) => params.id),
switchMap((id) => this.userService.fetchById(id)),
switchMap((user) => this.preferenceService.fetchForUser(user.uuid)),
)
.subscribe((preferences) => (this.userPreferences = preferences));

Prevent duplicate requests

Use shareReplay(1) pipes in your HTTP streams to avoid redundant HTTP-calls. This makes sure late subscribers get the latest value from cache. More info can be found here.

This is especially useful for expensive resources and/or if you know the contents of a resource will not change.

const resource$ = this.http
.get('/api/some-resource')
// The `1` parameter makes sure we only replay the last value, not all of them.
.pipe(shareReplay(1));

// Only first subscriber will trigger API call
resource$.subscribe(this.handleResult);

// All later subscribers share the result of the first call
resource$.subscribe(this.handleResult);

This example code will only execute the HTTP-call if it does not already have a value in memory.

Use mappers to transform data

Oftentimes you will need to transform data for usage in your application. A good example is mapping some data from one format to another. Create a pure function that transforms your data, that you can use inside a map operator in your pipe.

Example

This example shows a list of items from an observable. Imagine getting a list of objects of type Movie that needs to be displayed inside a dropdown list that expects an array of ListItem objects with name and value fields to work with a select input.

Create a reusable mapper function to transform items of type Movie into items of type ListItem:

mappers/movies-to-list-item.ts:

// First, create the singular mapper
const movieToListItem = (movie: Movie): ListItem => {
return {
name: movie.title,
description: moment(movie.releaseDate).format('LL'),
image: movie.poster,
};
};

// Secondly, create the plural mapper, using the singular mapper above
const moviesToListItems = (movies: Array<Movie>): Array<ListItem> => {
return movies.map(movieToListItem);
};

And use it in your favorite-movies.component.ts to map your items:

import { moviesToListItems } from 'mappers';

@Component()
class FavoriteMoviesComponent {
// Inject the service
readonly #userService = inject(UserService);

// Map movies to list items and assign to internal items$ property
readonly items$ = this.#userService.favoriteMovies$.pipe(map(moviesToListItems));

// Or, do both at once:
readonly items$ = inject(UserService).favoriteMovies$.pipe(map(moviesToListItems));
}

And display the items in the favorite-movies.component.html template using the async pipe:

<ui-list-item *ngFor="let item of items$ | async" [item]="item" />

Or, more advanced, with empty and loading states:

<ng-container *ngIf="items$ | async as items; else loadingState">
<ng-container *ngIf="items.length; else emptyState">
<ui-list-item *ngFor="let item of items" [item]="item" />
</ng-container>
</ng-container>

<ng-template #emptyState>
<p>No favorite movies found.</p>
</ng-template>

<ng-template #loadingState>
<p>Loading your movies...</p>
</ng-template>