Using Common RxJS Operators
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 take
s. 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 take
s. 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 take
s. 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();}