Home Java Detailed description of RxJS operators – Part 1

Detailed description of RxJS operators – Part 1

by admin

This article is a translation of the original article by Ben Lesh " RxJS Operators In-Depth – Part 1 ". I also run a telegram channel " Frontend in Flotus style ", where I talk about interesting things from the world of interface development

What is an operator? Why do they exist? Observables are "sets."

The first thing to understand about observables is why they exist. They exist because observables as a type allow us to treat events (or values over time) as sets or sets of things.

More simply put, any well-defined set will have operations that can be performed on it that can transform it into a new set of the same type. For example, suppose we have a truckload of apples. We could turn it into a truck with sliced apples using an apple slicing machine. The same apple slicing machine could then be used on any apple truck to turn it into a truck loaded with sliced apples. In this case, the apple slicing machine would be considered the "operator" who matches the apples to the apple slices. Similarly, we could have a truck with sugar, flour, eggs, etc. And combine them with an apple slice truck to make an apple pie truck using some kind of pie making machine. So, in this example, the truck is a set type, the apple slicing machine or pie making machine would be the "operators" and the sugar, apples, apple slices, eggs, etc. will simply be values carried by our set type.

All set types have different operations that can be applied to them. For example, one of the most common types of sets that we use are arrays. JavaScript arrays can be displayed, reduced, filtered, combined, expanded, etc. Arrays are one-dimensional, finite, synchronous sets of values with their own unique properties.

Observables are sets of events or values. This makes observable objects two-dimensional, which send a linear set of values that can be mapped, reduced, filtered, combined, expanded, etc., Like arrays; But values can arrive asynchronously, adding an extra dimension of time. The temporal nature of observables means that operations on them can include things like delays, timeouts, completion notifications, etc. This is why there are so many more possible operations on observables than, for example, on arrays.

Operators are mechanisms that can perform an operation on an observable, transforming it into a new observable.

Implementation of a base operator

A basic operator implementation is any function that takes an observable source and returns a new observable result that consumes the source. That is, when you subscribe to a result, you subscribe to the source. So, a very simple operator that doubles the numbers from an observable might look like this :

Basic operator "doubling"

import { Observable, of } from 'rxjs';const double = (source: Observable<number> ) =>new Observable((subscriber) => {const subscription = source.subscribe({// Here we alter our value and "send it along" to our consumer.next: (value) => subscriber.next(2 * value), // We have to make sure errors and completions are also forwarded to the consumer.error: (err) => subscriber.error(err), complete: () => subscriber.complete(), });return () => {// We must make sure to tear down our subscription.// when the returned observable is finalized.subscription.unsubscribe();};});// Usage like so:of(1, 2, 3, 4).pipe(double).subscribe(console.log);// Output:// 2// 4// 6// 8

What is Observable.prototype.pipe?

At this point there may be some confusion as to what this pipe method is in our observable above. The short version? Pipe does nothing but pass an instance of observable to any operator functions ((source: Observable ) => Observable )) that you pass to it, in order, passing the return value of each one to the next operator function in the chain. For example, we could use our double operator above more than once by simply passing it twice to the pipe method:

of(1, 2, 3, 4).pipe(double, double).subscribe(console.log);// Output:// 4// 8// 12// 16

This is exactly equivalent to this, because each pipe operator simply returns a new observable result :

const doubled = of(1, 2, 3, 4).pipe(double);const doubledAgain = doubled.pipe(double);doubledAgain.subscribe(console.log);// Output:// 4// 8// 12// 16

Creating an overused operator with higher order functions

What if we wanted to create an operator that could do more than just "double, " what if we wanted to be able to "triple" or multiply by an arbitrary number? Functional programming and "higher order functions" simplify this. Simply put, a "higher-order function" is a function that returns a function. So, in our case, we could create a multiplication operator that would allow us to pass a multiplier to it, using our double operator as the basis, such as :

import { Observable, of } from 'rxjs';const multiply = (multiplier: number) => (source: Observable<number> ) =>new Observable((subscriber) => {const subscription = source.subscribe({next: (value) => subscriber.next(multiplier * value), error: (err) => subscriber.error(err), complete: () => subscriber.complete(), });return () => {subscription.unsubscribe();};});// Usage like so:of(1, 2, 3, 4).pipe(multiply(2)).subscribe(console.log);// Output:// 2// 4// 6// 8

Moreover, with this functional programming practice you can recreate double and reuse it as in our previous example if you wish; This is done simply by calling the multiplication function and saving and reusing the returned operator function :

const double = multiply(2);const doubled = of(1, 2, 3, 4).pipe(double);const doubledAgain = doubled.pipe(double);doubledAgain.subscribe(console.log);// Output:// 4// 8// 12// 16

This brings us to a bit of confusing terminology, is multiplication an "operator"? Or does it return a "operator"? This is sort of a focus on differences that are so small that they don’t matter, but in the basic RxJS command we generally refer to higher-order functions as operators, and their return values as "operator functions." Ultimately, how you refer to these things doesn’t matter if the people around you know what you’re talking about. (I’ve also heard "operator" and "operator instance.")

Operators are "declarative"

Yes, yes. Under the hood you have the necessary stuff. Either you necessarily add operator functions to the pipe, or however you pay attention to it. But operators are designed in such a way that you can move them around declaratively in the observed pipeline, and they can have different effects. For example, you have an operator to filter odd numbers. The results will be very different depending on where you put that pipe. But what is interesting is that you really don’t need to change the surrounding code, the operators "operate" on the incoming values without caring what is "superior" to them, except that the incoming type is correct (in this case a number) :

import { of, OperatorFunction, map, filter } from 'rxjs';const double = map((n: number) => 2 * n);const onlyEvens = filter((n: number) => n % 2 === 0);const source = of(1, 2, 3, 4, 5);source.pipe(onlyEvens, double).subscribe(console.log);// logs: 4, 8source.pipe(double, onlyEvens).subscribe(console.log);// Logs: 2, 4, 6, 8, 10

Going deeper : you create functions declaratively.

In my last post I showed that observables objects are just specialized functions. Hence, if RxJS operators are used to wrap an observable source with a new observable result, it means that you are essentially taking a specialized function and wrapping it in a new specialized function of the same form that will call the original. So basically you’re using functional programming to declaratively create new functions. It’s really just a different way of thinking about functions, I’m not really asking any of you to create such functions – imperative code is much better for constructing arbitrary functions – but consider naming statements and observable based on what they do, not just what they send out. It’s just something to think about right now. These concepts will become clearer when you start to better understand side effects and their relationship to functions (and therefore observable). I’ll probably write more about this in future articles.

Important tips

When creating your own operators :

  1. Operators must be thoroughly vetted. (I will talk about this in the next post).

  2. If possible, try to reuse the RxJS operators. They are very well tested and cover a lot of extreme cases that you may not have even known about (see Tip #1).

  3. Always check if your operators send all 3 types of notifications correctly: "next", "error" and "complete".

  4. Always make sure that you clear all subscriptions created by your operators.

  5. Operators usually should not (almost never) create subscripts outside of the returned observable.

You may also like