Original cover photo by Altum Code on Unsplash.
When to use operators?
In the first part of this series, we explored use cases of different RxJS functions used to combine Observable
streams.
In the second installment, we are going to take a look at different RxJS operators, understand how they work and in which scenarios
are they applicable.
As experience shows, we can have knowledge about the existence and functionality of certain operators, but sometimes it is
hard to spot that a certain problem can be solved by using a particular operator.
So, with that in mind, let's get started!
Waiting for other Observable
s
debounceTime: wait for a quieter time
Perhaps one of the iconic use cases of an RxJS operator is debounceTime
. debounceTime
is an operator that allows us to wait until the emissions of an Observable
have paused for a certain while, and only then emit the latest value. This allows us to prevent multiple handlings of the same event until the situation has settled. A great example of this is implementing an HTTP API call to search using the text being typed by the user. Of course, we would have to listen to the input
event, but it makes no sense to make a call every time the user presses a key on their keyboard. To avoid this, we can use debounceTime
to make the call when the user finished typing. Here is a small example:
const input = document.querySelector('input');
fromEvent(input, 'input').pipe(
debounceTime(500),
// wait for 500ms until the user has finished typing
switchMap(event => filterWithAPI(event.target.value)),
// make the http call
).subscribe(result => {
// handle the result
});
So this is one example of how time based operators can be used to make our app more efficient. Use this if you want to perform something only after a period of silence.
auditTime: handle something only once in a while
auditTime
is a specific operator, which, provided with a period of time, only emits the latest value once in that time frame.
This may seem very specific, but we can come up with good use cases. Imagine the following scenario: we have an app that displays
a graph of a stock exchange. We are connected with the server via a websocket, which provides us with real time data about the stock market.
Now because the market can be volatile, we may get many emissions, especislly if we display several graphs, we might get multiple emissions in just several seconds. Now repainting the graph faster than every second can be a costly process (canvas can be memory heavy), and also might be confusing
for the end user. So, in this scenario, we might want to repaint the graph every one second. Here is how we can do it using auditTime
:
observableWhichEmitsALot$.pipe(
auditTime(3_000),
// maybe other operators that perform costly operations
).subscribe(data => {
// maybe a heavy repaint
});
So here we use auditTime
for better and controlled performance.
distinctUntilChanged: preventing unnecessary operations
We can improve the previous example even further, if, for example, our source might send data that is duplicate in a row. It does not even
have to be completely different: sometimes we only care about some keys in the emitted object. In this case, we might want to prevent other heavy operations by using distinctUntilChanged
with a specific condition:
observable$.pipe(
distinctUntilChanged((prev, next) => {
return (
prev.someKey === next.someKey ||
prev.otherKey === next.otherKey
// maybe some other conditions
);
}),
);
Now, paired with the auditTime
from the previous example, we can use this to boost performance, aside from other use cases.
If you only care about one key change, you might want to use
distinctUntilKeyChanged
timestamp: you need to display the time when data arrived
Imagine you have an application where you receive data continuously (maybe via a WebSocket), and display it as soon as it arrives.
But it is important for the user to know when the data has arrived (maybe when the message was received, for example). In this case, you might want to use the timestamp
operator to attach the arrival time on a notification:
observable$.pipe(
timestamp(),
).subscribe(({value, timestamp}) => {
console.log(new Date(timestamp));
// will log the datetime
// when the notification arrived in UTC
});
This can also be used if we aggregate values (with the buffer
operator for example) to monitor the intensity of emissions (which times of day we receive most notifications, for example).
toArray: you want to map lists of data
Lots of applications (especially in Angular) use Observable
s instead of Promise
s to handle HTTP calls. But sometimes we want to slightly modify the response before using it in the UI. And when the response value is an Array
, it might become a bit messy from the
code perspective, if we want to map each item, but still emit an Array
. Here is how toArray
, in combination with swichMap
, can help:
responseFromServer$.pipe(
switchMap(response => response.data),
// switch to the data array, so that it emits each item
map(item => {
// here we can perform each mappings on each item of the array
}),
toArray(), // when the items are done,
// collect all of them back to an array,
// and emit it
); // now we have an Observable of an array of mapped items
retry: handling errors when we deem necessary
Errors are a natural part of any application: whether the server failed to deliver a successful result, or there is some inconsistency
in our frontend app, we want to handle errors gracefully, and, if possible, still try to deliver the desired result to the user anyway.
One way to achieve this is using the retry
operator; this operator will try to work the Observable
(an HTTP request, for example)
again, as many times as we wish, until it succeeds. Here is an example:
responseFromServer$.pipe(
retry(3), // try again 3 times
); // after 3 failed attempts, will finally fail and send an error
But what if we have a specific situation? For example, we show an error notification, and it contains a button the user clicks on to try again?
Now we can provide a config ro the retry
operator to specify another Observable
to wait for:
responseFromServer$.pipe(
retry({
count: 3, // we can also OPTIONALLY
// provide how many times
// a user is allowed to retry
delay: () => fromEvent(
document.querySelector('#retryBtn'),
'click',
), // wait for the user to click the button
}),
);
Now, the retry
operator will wait for the user to click the button, and will try again the Observable
until it succeeds.
This can become very useful specially in the case if we use Angular and some state management that provides for side effect management via RxJS, but can also be applicable in any other application.
We can do the same with handling completed
Observable
s instead of error, for example, reopening closed WebSockets, using therepeat
operator with configs.
What’s next?
In the second part, we examined use cases for operators that usually perform routine tasks, like error handling and data manipulation.
In the next and final article, we are going to examine use cases for operators and entities that accomplish more obscure, but still useful tasks, including Schedulers
, Subjects
and so on.
Top comments (1)
Great article!
Noticed one thing: in the
toArray
example on this line:switchMap(response => response.data)
Should it look like this instead?
switchMap(response => from(response.data))
As
switchMap
should turn the array into an observable emitting items one by one.