DEV Community

Cover image for Understanding RxJS use cases (part II)
Armen Vardanyan for This is Learning

Posted on

Understanding RxJS use cases (part II)

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 Observables

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
});
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

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
        );
    }),
);
Enter fullscreen mode Exit fullscreen mode

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 
});
Enter fullscreen mode Exit fullscreen mode

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 Observables instead of Promises 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
    }),
);
Enter fullscreen mode Exit fullscreen mode

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 Observables instead of error, for example, reopening closed WebSockets, using the repeat 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.

Discussion (0)