Using Common RxJS Operators

Laura Slocum
7 min readMay 9, 2022

RxJS can seem intimidating when you start working with Observables. I have created this cheat sheet to remember what each operator does and when to use them. I hope it helps you too!

Note: This document is meant to provide some clarity around some commonly used RxJS operators. This is not an exhaustive list of operators.

Glossary

Source Observable: The observable starts the pipeline. In the following example, the “source observable” is this.logicService.getUser()

this.logicService.getUser(userId).pipe(concatMap(user => this.logicService.getUserRoles(user.recruiterId)));

Inner Observable: Any observables that may exist inside your pipeline. In the above example, the “inner observable” is this.logicService.getUserRoles()

Mappers

Map

Learn how to use this operator.

Transforms the value returned in the stream and returns the new value.

Usage Notes

Must return a value.

When to use

When you need to transform the data.

Example

const username: Observable<string> = this.logicService.getUser(userId).pipe(map(user => user.name));

To get the current user, this will call the API (brokered through the logic service). We only care about the user’s name, so we use map to transform the returned object into the value we need. Just the user’s name is returned from this stream.

concatMap

Learn how to use this operator.

When the source observable completes, the inner observable is subscribed to. If you chain `concatMap` in your pipeline, they will execute in order.

Usage Notes

Must return another observable.

When to use

When you need the value of a previous call to make this call & the order of emission and subscription of inner observables is important.

Example

const userRoles: Observable<Roles> = this.logicService.getUser(userId).pipe(concatMap(user => this.logicService.getUserRoles(user.recruiterId)));

To get the current user, this will call the API (brokered through the logic service). After the user call has been completed, the call to get user Roles is made. Roles are returned from the stream.

mergeMap / flatMap

Learn how to use this operator.

This operator is best used when you wish to flatten an inner observable but want to control the number of inner subscriptions manually.

`mergeMap` allows for multiple inner subscriptions to be active at a time. Because of this, one of the most common use-case for mergeMap is requests that should not be canceled; think writes rather than reads.

Usage Notes

Must return another observable.

When to use

When you need to make a POST, PUT, or DELETE call.

Example

const userRoles: Observable<Roles> = this.logicService.saveUser(user).pipe(mergeMap(user => this.logicService.updateUserRoles(this.userRoles, user.userId)));

This pipeline will call the API (brokered through the logic service) to save the user. At the same time, another call is made to update the user’s roles. If the user clicks save again before the pipeline completes, another save will be initiated; it will not cancel the first save.

switchMap

Learn how to use this operator.

The main difference between switchMap and other flattening operators is the canceling effect. On each emission, the previous inner observable (the result of the function you supplied) is canceled, and the new observable is subscribed. You can remember this by the phrase switch to a new observable.

This works perfectly for scenarios like typeahead, where you are no longer concerned with the response of the previous request when a new input arrives. This also is a safe option in situations where a long-lived inner observable could cause memory leaks, for instance, if you used mergeMap with an interval and forgot to dispose of inner subscriptions properly. Remember, switchMap maintains only one inner subscription at a time.

Be careful, though. You probably want to avoid switchMap in scenarios where every request needs to complete; think writes to a database. switchMap could cancel a request if the source emits quickly enough. In these scenarios, mergeMap is the correct option.

Usage Notes

Must return another observable.

When to use

When you do not care about previous values, if a new value is emitted from the source observable. It is best used for GETs and typeahead's because of the canceling effect.

Example

const userRoles: Observable<Roles> = this.logicService.getUser(userId).pipe(switchMap(user => this.logicService.getUserRoles(user.recruiterId)));

To get the current user, this will call the API (brokered through the logic service). After the user call has completed, the call to get user Roles is made. If the user refreshes the component, the API calls are canceled, and new observables are emitted. Roles are returned from the stream.

Utilities

tap

Learn how to use this operator.

Perform a side effect for every emission on the source Observable but return an Observable that is identical to the source.

Usage Notes

Does not return a value. Use only for side effects. Do not use it to set up other observables. The following is incorrect.

const userName: Observable<Roles> = this.logicService.getUser(userId).pipe(tap(user => (this.roles$ = this.logicService.getUserRoles(user.recruiterId))));

Evaluate one of the map operators and setup variables you may need. For example, if you really need the user’s name to be returned from this pipeline, you could do something like:

const userRoles: Observable<Roles> = this.logicService.getUser(userId).pipe(switchMap(user => {this.roles = this.logicService.getUserRoles(user.recruiterId);return user;}));

When to use

When you do need to perform a side effect like log a message.

Example

const userRoles: Observable<Roles> = this.logicService.getUser(userId).pipe(switchMap(user => this.logicService.getUserRoles(user.recruiterId)),tap(roles => { console.log({roles}); }));

After the API calls have been resolved, the roles will be logged to the console. Roles are returned from the stream.

finalize / finally

Learn how to use this operator.

Execute a callback function when the stream completes or errors.

Usage Notes

Like the tap this operator should be used to perform side effects. The difference is that finalize will execute even if the observable returns errors.

When to use

When you do need to perform a side effect regardless of a successful call or not, like turning off a loading spinner.

Example

const userRoles: Observable<Roles> = this.logicService.getUser(userId).pipe(switchMap(user => this.logicService.getUserRoles(user.recruiterId)),finalize(roles => { this.isLoading = false; }));

When the observable completes (successfully or not), it will turn off the loading spinner. Roles are returned from this stream.

Error Handling

catch / catchError

Learn how to use this operator.

Execute a callback function when the stream errors.

Usage Notes

Must return an observable.

When to use

When you want to handle errors gracefully.

Example

const userRoles: Observable<Roles> = this.logicService.getUser(userId).pipe(switchMap(user => this.logicService.getUserRoles(user.recruiterId)),catchError(error => this.modalService.openErrorDialog(error)));

The observable errors will show an error dialog to the user. undefined is the value of our constant.

retry

Learn how to use this operator.

Retry an observable pipeline a specific number of times should an error occur.

Usage Notes

Must specify the number of times to retry the call.

When to use

When the observable fails, but you want it to retry x number of times before it gives up.

Example

const userRoles: Observable<Roles> = this.logicService.getUser(userId).pipe(switchMap(user => this.logicService.getUserRoles(user.recruiterId)),retry(2),catchError(error => this.modalService.openErrorDialog(error)));

When the observable errors, it will try the call 2 more times. If it fails on the 3rd try, it will show an error dialog to the user. `undefined` is the value of our constant.

retryWhen

Learn how to use this operator.

Retry an observable pipeline based on the return value of a callback function should an error occur.

Usage Notes

Must return an observable.

When to use

When the observable fails, and you want it to retry after a specific amount of time before giving up.

Example

const userRoles: Observable<Roles> = this.logicService.getUser(userId).pipe(switchMap(user => this.logicService.getUserRoles(user.recruiterId)),retryWhen(error => error.pipe(delay(5000)),catchError(error => this.modalService.openErrorDialog(error)));

When the observable errors, it will try the call again after 5 seconds. It will continue to retry until the observable no longer errors. The catchError block will not be hit.

Filtering

filter

Learn how to use this operator.

Only emits values that pass the provided condition.

Usage Notes

Must return a Boolean.

When to use

When you only care about the emitted value if it meets certain criteria. I use it most often to filter out emissions before a store is hydrated.

Example

const userRoles: Observable<Roles> = this.store.selectUser().pipe(filter(user => !!user?.name),take(1),switchMap(user => this.logicService.getUserRoles(user.recruiterId)));

If the user is a falsely value, then the inner observable is not executed. When the store emits a new value with a user that has a property of name with a truthy value (remember empty objects are truthy), then will take the first value (ending the subscription) and continue in the pipeline. User roles are returned from this pipeline.

find

Learn how to use this operator.

Emit the first item that passes the predicate, then complete.

Usage Notes

Must return a Boolean.

When to use

When you are listening for a specific value to be emitted.

Example

const userRoles: Observable<Roles> = this.store.selectUser().pipe(find(user => user?.name === ‘Laura’),switchMap(user => this.logicService.getUserRoles(user.recruiterId)));

take

Learn how to use this operator.

Emit provided a number of values before completing.

Usage Notes

Remember, pipelines execute in order. In the following example, we will take the first emission and will unsubscribe even if the first emission is undefined.

this.store.selectUser().pipe(take(1),filter(user => !!user?.name),).subscribe();

API calls complete immediately after the call returns. There is no need to use a take on an API call.

take is the opposite of skip. For every take operator, there is a skip operator available. I will only document takes. You can refer to the RxJS site for skip documentation.

When to use

When you are only interested in the first x number of emissions. We usually use `take(1)` because we only care about the first emission.

Example

const userRoles: Observable<Roles> = this.store.selectUser().pipe(take(1),switchMap(user => this.logicService.getUserRoles(user.recruiterId)));

takeWhile

Learn how to use this operator.

Emits values until the provided expression is false. This is the opposite of filter.

Usage Notes

takeWhile is the opposite of skipWhile. For every take operator, there is a skip operator available. I will only document takes. You can refer to the RxJS site for skip documentation.

When to use

When you want the observable to stay open until the component is destroyed.

Example

alive = true;const userRoles: Observable<Roles> = this.store.selectUser().pipe(takeWhile(user => this.alive),switchMap(user => this.logicService.getUserRoles(user.recruiterId)));ngOnDestory() { this.alive = false; }

takeUntil

Learn how to use this operator.

Emit values until provided observable emits.

Usage Notes

Must return an observable.

takeUntil should be the last operator in the pipeline to avoid memory leaks.

takeUntil is the opposite of skipUntil. For every take operator, there is a skip operator available. I will only document takes. You can refer to the RxJS site for skip documentation.

When to use

When you want to keep an observable open until the component is destroyed.

Example

destroy$: Subject<boolean> = new Subject<boolean>();const userRoles: Observable<Roles> = this.store.selectUser().pipe(switchMap(user => this.logicService.getUserRoles(user.recruiterId)),takeUntil(this.destroy$));ngOnDestroy() {this.destroy$.next(true);this.destroy$.unsubscribe();}

--

--

Laura Slocum

Software Architect and UI/UX Team Lead. Learning is my never-ending story.