RxJS is a JavaScript & TypeScript library for handling asynchronous events and streams of data. It provides powerful functionality for working with observable events and does what Promises alone cannot. This library is leveraged by frameworks such as Angular and is widely used to satisfy modern JavaScript design requirements.
Core Concepts
The following four concepts form the foundation for working with observables with RxJS:
- Observable: An object which will emit one or more values at some point in the future.
- Subscription: A callback assigned to an observable which is invoked when the observable emits an event.
- Observer: A consumer of the observable who subscribes to it and invokes some callback when an event is emitted.
- Subject: An observable-like object which can be told to emit events similar to an EventEmitter.
Observables vs Promises
The following table outlines some of the similarities and differences between promises and observables.
Promise | Observable | |
---|---|---|
Events | Emits a single event before completion. | Emits any number of events before completion. |
Reacting to Events | Promises provide the following chainable callbacks: then, catch, finally. These callbacks will be invoked once when the Promise completes. | Any number of callbacks can be piped to an observable to transform the data, filter certain emissions, and more. |
Operating / Transforming | Values can be manipulated in callbacks provided to then and catch. Those methods themselves are chainable. | Observables can be piped with operators such as map and filter. Unlike promises, piping is a separate step from subscribing. |
Async / Await | Promises can be automatically boxed and unboxed using the async / await syntax. | Observables cannot take advantage of async / await unless they are first converted to promises. |
Subscribing and Unsubscribing
The subscribe method of an observable is similar to the then method of a promise. However, when subscribing to an observable, the return type of the subscribe method is a subscription, unlike a promise, where calling then returns another promise. The process of subscribing and unsubscribing is as follows:
- Subscribe: subscribe to an observable by calling it's subscribe method with a callback function as the method's first argument. Note: the callback will be invoked every time the observable emits a value.
- Subscription: the subscribe method returns a subscription object which can be used to unsubscribe in the future.
- Unsubscribe: when the subscription should be stopped, such as when an object reaches the end of its lifecycle, the subscription's unsubscribe method should be called. The method takes no arguments. If this is not called when it needs to be, memory leak issues may arise.
- Completion: some observables will emit a complete event. When this happens, all subscriptions will be unsubscribed automatically.
A common practice for managing subscriptions is to create an empty array of subscriptions when an object is created, push new subscriptions to that array as they are created, then when it is time to unsubscribe, iterate over that array and call the unsubscribe method for each item. For example, an Angular class might do something like this:
Transforming an Observable with Operators & Pipes
When we apply pipes to an observable, the result will be a new observables. Piping observables does not affect the source observable itself. There are dozens of different operators, and you can also make your own; the table below outlines some of the most common ones.
filter | Block events where the emitted value does not meet some condition. | |
---|---|---|
map | Transform the values emitted from the original observable. | |
switchMap | Transform the values emitted from the original observable, but return an observable instead of a value. | |
catchError | If an error is thrown by the observable, handle the error and return a different observable. | |
take | Take up to n elements from the observable, then close the subscription automatically. |
Note: piping an observable is not the same as subscribing to it. If the observable has a pipe but no subscriptions, it will still behave as if it has no subscriptions.
The example shown below will operate on an observable which emits numbers, filter out even numbers, then multiply by 2. The pipes are created by providing callbacks into each pipe individually, and the pipes themselves are provided as separate arguments to the observable's .pipe method. Each pipe will be invoked in the order in which it is declared.
Combining Observables
Often times it is useful to combine two or more observables into one. A few common use cases are given below:
- Consolidate Network Calls: we can combine the responses of multiple network calls into one observable, then take some action when that observable emits an event (and thus all network calls are complete).
- Have a Common Response to any Event / State Change: it may be useful to take some common action when the user interacts with the page in any manner, such as clicking on any input field. If we have one observable for each input field, we can combine them all into a new single observable, then subscribe to that, so we can avoid having a separate subscription for each input control.
- Combining Events of the Same Type from Different Instances: if there is a case where events from multiple instances of an object or class need to be consolidated, we can combine the events into one observable.
A few common ways of combining observables are given below. Each has different behavior.
merge | Combine multiple observables into one. When any observable emits a value, the merged observable will also emit that value. | |
---|---|---|
combineLatest | This behaves in the same way as merge, except the emitted value will be an array of all of the most recent values of each observable. This will not emit any value unless all source observables have emitted a value at least once. | |
forkJoin | When all observables provided complete, this will emit a single event with an array of all of the last values emitted by each provided observable before completing. |
Converting To and From Promises
It may be useful to convert a promise to an observable, or an observable to a promise.
- Promise to Observable: RxJS provides a function called from which accepts a Promise as input and returns an observable. This observable will only emit one event, then complete.
- Observable to Promise: observables have a method toPromise which will return a Promise. This promise will complete when the observable itself completes and returns a value. If the observable never completes and keeps emitting values, the promise object will never resolve. Therefore, it may be a good idea to use the take operator in this case.
Working with Subjects
A simple way to create an observable is to work with a subject object. A subject is an object which provides a subscribe method and a next method. The next method, when called, emits the value it has just been given. This provides a convenient way to create an observable; all details about managing subscriptions is encapsulated by the subject internally. The code below gives an example of subscribing and emitting.
There are multiple types of subjects; the two simplest and most commonly used are:
- Subject: subscriptions are triggered only when the subject's next method is called.
- Behavior Subject: subscriptions are triggered as soon as they are created with the last value emitted from the subject. Subsequent emittions also trigger existing subscriptions. This subject is also initialized with some default value in its constructor.
Managing Subscription
When using RxJs, we must make sure that we handle the lifecycle of our subscriptions properly. Otherwise, we risk introducing memory leaks and lifecycle related defects into our application. However, a detailed discussion on this topic falls outside of the scope of this article. To learn more about this, please visit our dev page on the topic: