Grasp RxJs: Basic operator types
By: Lars Holdaas
Tags: Front End Development, Web Development, JavaScript, Reactive Programming, Rxjs

Photo by Alexandre Debiève on Unsplash

Operators are the bread and butter of RxJs. Take an observable, pipe it through a handful of operators, and suddenly you have written a precise and exact reactive procedure. After all, reactive programming is all about writing reactions to events.

Although it’s possible to learn operators by going through each one of them and reading the documentation, I’ve found that with developers fresh to RxJs the first step really should be to have a clearer idea of what an operator can and can’t be. There are actually many different classifications of operators, and how some function might completely blow you out of the waters!

Note that many operators belong to more than one of these classifications. Operators that are purely one type are quite rare, and it is exactly the way that some operators combine behavior from various classes of operators that make them so useful and versatile, and therefore powerful. It might seem a bit overwhelming at first, but consider this article more of an introduction to the different types than a deep technical explanation.

This article will only look at the simplest operator types. By simple I mean operations that should more or less be familiar to all programmers regardless of their understanding of RxJs. This means that operators that change how the source Observable behaves in a way that requires a deeper understanding of Rx will not be included.

So without further ado, let’s take a look at various different types of operators that exist in RxJs.

Stateful operators

Some operators maintain an internal state that influences how it reacts to an emission, others don’t. Examples of those that don’t include for instance map . Note here that it is completely possible, though generally not advisable, to reference and mutate variables outside a mapping function used in map , however keeping state is not built-in feature of map .

Contrast this to for instance pairWise , which works by returning the previous and the current emission as a two-element array. In other words, pairWise always keeps track of the value of the previous emission that reached the operator.

Another example is distinctUntilChanged which compares the previous emission and the current emission to see if they should be considered equal or not. Similar to pairWise , distinctUntilChanged must then also keep the value of the last emission to be able to perform the comparison.

scan is an operator that functions like Array.reduce . It takes a function as its first parameter that has the previously returned value and the value of the current emission as parameters. When the function returns, the new value is simultaneously emitted and stored in the operator for the next calculation. Like Array.reduce , it even takes the starting value as its second parameter!

Transforming operators

Transforming emissions is probably the kind of operation the average RxJs plumber will do more than anything else using RxJs. The most used emission-transforming operator will usually be map , which simply takes a mapping function as its parameter. The mapping function describes what the emitted value should be coming out based on what the emitted value is coming in.

Another related operator of this type is mapTo , which can be used when the incoming value does not affect the output in any way. Like let’s say at some step, we always want to change the emission to true. If so, we can simply use mapTo(true) . Note that map takes a function as its parameter, which will always be run when an emission reaches, whereas the parameter to mapTo should be an already calculated value.

scan also qualifies as an emission-transforming due to changing the value of the emission. distinctUntilChanged falls into another category of filtering types which we will look at next, as it never actually changed the value of any emission, but rather filters out duplicates until a value unlike the previous is emitted.

Similarly, pairWise can be considered a filtering operator on the first emission and a transforming operator from the second emission on. The reason is that pairWise does not emit the first value it receives, but waits until it actually has a pair to emit.

(If you’re ever in the case where you want to use pairWise but want it to pass through emissions from the first, you can solve this by putting a startWith before pairWise with whatever you want the initial default value to be. That default value will then be treated as the previous emission when the first real emission reaches the pairWise operator.)

The standard example of a non-transforming would be tap . tap , in the eyes of the pipe, does absolutely nothing. It does not affect the pipe or the emissions in any way by default. tap is generally used for side-effects such as logging, but can also be useful for activating other reactive signals in certain cases. However be careful of using a Subject.next inside tap too often; it might be a sign that your Subject should’ve been an Observable and written as a reaction instead.

Filtering operators

This group of operators work by removing emissions entirely rather than mutating them. Any operator in the pipe after a filtering operator will never react to the emissions that are filtered out; it will be as though they never existed.

The most obvious example is filter which takes a predicate function that has the emitted value as its input, and the operator passes on the emission when the predicate returns true .

last is an interesting operator that can be considered both filtering and stateful. It filters out all emissions until the source stream completes and then forwards only the value of the last emission that reached it.

The most extreme filtering operator is ignoreElements . ignoreElements filters out all emissions, letting the subscriber only ever receive success or failure signals from the source stream. While this might not seem particularly useful, it’s very helpful when you want to only signal the start and end of an active pipe, using for instance ignoreElements , startWith and endWith on the same source stream. In this case, startWith can be used to declare the value you want to emit on stream instantiation and endWith the value you want to emit on completion, and you can then ignore all actual emissions.

Delaying operators

The delaying operators in general introduce asynchronicity to pipes that might or might not be otherwise synchronous pipes. One of the most common delaying operators is debounceTime . debounceTime can be considered both a delaying and a filtering operator as it acts by filtering out any previous emission once it receives a new one within the specified delay period. Once the delay period has passed, the emission continues down the pipe. This is especially useful for handling user input.

We also have simply delay which works much like setTimeout . Just set how many milliseconds you want to wait until the emission should continue down the pipe and you’re good. Like setTimeout , delay also accepts 0 as a parameter which simply forces the operation to be asynchronous without any delay (can be useful for solving race conditions, but please use this sparingly).

Completing operators

Although this article will not go deep on operators that are more meta-RxJs, the concepts of completing should be simple enough that we will cover this topic here. An Observable completing means it has exhausted all its potential emits and will no longer be active. From a seasoned programmers perspective, considering this the same as a graceful termination would be accurate.

The simplest kind of completion is when you’re creating an Observable using for instance of on some data. In this case, the Observable consists of that single value, and any subscription to this will immediately receive the value as an emission and a completion signal. Likewise, for can split up an array into one emission per value, emitting them and a completion signal synchronously.

However, not all Observables have a clear point at which it completes by itself. In fact, many Observables do not have any completion built-in, like any Observable created using interval . In these cases, we often want to use operators to force a completion of the pipe.

Completing operators are operators that force a pipe to complete based on some condition. last ’s counterpart first is a great example of this. first , without any parameters, simply emits the first value of its source stream and then completes. Instead of filtering out the subsequent emissions, they simply do not exist anymore as the observable stream has completed and is no longer active.

first is even more interesting with its parameter. It takes a predicate that uses the emitted value as a parameter and only passes on an emission when the predicate returns true . However, once it does, it immediately completes the stream. In other words, it goes from being a filtering operator to a completing operator once it receives a value that satisfies the predicate.