Unusual RxJS

Another article - another cat of Istanbul.  This time - unusual =)

Another article – another cat of Istanbul. This time – unusual =)

Hi all! Did you know that RxJS contains more than 100 operators? But if your project uses this library, most likely you will hardly have more than a couple of dozen. Interesting situation, right? I don’t know why this happens, but today I want to share real examples of using “rare” operators. Let's get started!

Are you ignoring me?

Our first statement is called ignoreElements. It literally does the following: it skips (ignores) all elements of the stream. In other words, after ignoreElements the next callback never fires, only error or complete will fire. Let's see an example.

Let's say we have a server that we need to reboot. The server status can be online, offline, restart or error:

export enum ServiceStatus {
 online="online",
 offline="offline",
 restart="restart",
 restart="error",
}

The getServerStatus method gets the server status, say, from the NgRx store. We need to wait until the server is online again and show a message about a successful reboot.

waitForRestart = (): Observable<never> =>
 this.getServerStatus().pipe(
   filter((status) => status === ServiceStatus.online),
   take(1),
   ignoreElements()
 );

Note that waitForRestart is of type Observable. It will not miss any value. We filter only the online status, and after receiving it, we end the stream using take(1).

Next we need to process the error status. In this case, we will end the stream with an error:

private processErrorStatus = switchMap((status: ServiceStatus) =>
  status === ServiceStatus.error
    ? throwError(new Error('Error occurred during restart'))
    : of(status),
);

Putting it all together:

waitForRestart = (): Observable<never> =>
 this.getServerStatus().pipe(
   this.processErrorStatus,
   filter((status) => status === ServiceStatus.online),
   take(1),
   ignoreElements()
 );

Let's use the resulting function:

private showMessageAfterRestart = () =>
 this.waitForRestart().subscribe({
   complete: () => this.showSuccessMessage(), // сообщаем об успехе
   error: () => this.showErrorMessage(), // уведомляем об ошибке
 });

So we've created a clean and simple waitForRestart function that's easy to use. Let me emphasize once again that we only use the complete and error fields. Next will not return any messages thanks to ignoreElements.

There's a pair for each creature

The next operator is pairwise. It is used more often than ignoreElements, but still not very often. Starting from the second stream value, pairwise sends 2 values ​​- current and previous. This can be useful in any situation where you need to keep track of what preceded the current value. Now, as usual, let's look at a real example.

Let's say that we have a list of notifications that appear in the upper left corner of the page when some event occurs in the application. All events happen in turn. Given a task: when we have more than one event, we need to show the previous one, which “goes” under the current one. This is a purely visual effect.

By the way, then you can add transition animations and it will be absolutely beautiful. But that’s not about that now, let’s see how to get the last 2 notifications from the stream.

currentAnnouncement$ = this.notificationsService.getLatest();  // текущая

previousAnnouncement$ = this.currentAnnouncement$.pipe( // предыдущая
 pairwise(),
 map(([previous, current]) => previous)
);

First we get the current notification. Next, we apply pairwise to it and get 2 elements per stream. All that remains is to use the map operator to select the previous element.

Who changed the keys?!

The distinctUntilKeyChanged operator is very similar to the familiar distinctUntilChanged. But in the case when you need to distinguish changed elements by some key (for example, by id), it will look more elegant in your code. Let's look at an example and everything will become clear right away.

Let’s say different users come to our stream (let’s use the of operator for simplicity):

users$ = of(
 { age: 22, name: 'Ivan', id: 1 },
 { age: 22, name: 'Ivan', id: 1 },
 { age: 27, name: 'Irina', id: 2 },
 { age: 24, name: 'Aleksey', id: 3 }
);

We set a task: we need to filter users so that there are no repetitions 2 times in a row.

What kind of example is this?

The attentive reader will notice that this example is more abstract than the others. This is true. However, I felt that for such a simple case it was not worth “chewing” and describing the business case in more detail.

User Ivan with Id 1 is repeated 2 times in a row. To get only unique values, let's use distinctUntilChanged:

this.users$.pipe(
  distinctUntilChanged() // не сработает!
);

Did not work! Since we are dealing with objects, simply using an operator without parameters will not work. The reason is that the entities we are working with are different objects and cannot be compared by value here. Correcting:

this.users$.pipe(
  distinctUntilChanged((a, b) => a.id === b.id)
);

Now we pass a function that compares users by id. But I would like to simplify this entry. And this is where distinctUntilKeyChanged comes into play:

this.users$.pipe(
  distinctUntilKeyChanged('id')
);

It turned out shorter and easier to read.

Take his example

The last operator for today is called sample. To better understand how it works, let's look at the official diagram:

taken from https://rxjs.dev/api/operators/sample

The operator receives two streams, and then releases the last value of the first stream at the moment when the second stream produces a value. As you can see in the diagram, the resulting stream releases a, then c, then d. It only skips values ​​when the second stream is triggered. At the same time, if since the last trigger a new value has not yet arrived in the first stream, nothing happens. The example also shows that b is not skipped, because between b and c nothing was received in the second stream.

It seems like we figured it out, but where can we use this in practice? Let's say we have a slider that allows you to enter a result from 1 to 10, and a save button:

By clicking on the “Save” button, we send the new value to the server. At the same time, we want that with several clicks the value is sent only once until the user changes it. And here sample will help us:

sliderChange$: Observable<number> = this.slider.getChanges();  // изменения слайдера

save$ = new Subject<void>();  // используем для кликов

saveSliderChangesOnClick = () => this.sliderChange$.pipe(
  sample(this.save$),
  distinctUntilChanged(),
  switchMap((value) => this.api.saveSliderValue(value)),
  takeUntil(this.componentDestroyed$)
).subscribe();

So, sliderChange$ is the first stream in our terminology. It is combined with save$ (second stream) using sample. Next, switch to the saving function using switchMap and you're done! The takeUntil operator ends the stream; usually the component destruction event is used for this.

distinctUntilChanged is needed for the case when the user changes the value, and then changes his mind and sets the old one. For example:

  • Last saved value 1

  • Change to 5

  • We changed our minds, change it back to 1

  • Save

As a result of such actions, nothing will be sent to the server thanks to distinctUntilChanged.

To finish the job, let’s code the “Save” button (an example for Angular, but this could be any framework):

<button type="button" (click)="save$.next()">Save</button>

That's it: sample allowed us to write some pretty neat logic in a very simple way.

Conclusion

Thanks to everyone who read to the end! Do you consider the operators I listed to be rare? What rare operators do you use in practice?

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *