Lettable Operators and RxJS Versioning

RxJS 5.5.0-beta.0 was released on 22nd of September and the main feature it introduces is the new way to apply operators on observables. This feature is called lettable operators. In this blog post I’ll explain what lettable operators are, what problems are they addressing and why I feel the way they are released is not optimal for the future of RxJS 5.

Lettable Operators

Let’s start by taking a look at what do the lettable operators look like:

import { range } from 'rxjs/observable/range';
import { map, filter, scan } from 'rxjs/operators'; // Point 1

const source$ = range(0, 10);

source$.pipe( // Point 2
  filter(x => x % 2 === 0),
  map(x => x + x),
  scan((acc, x) => acc + x, 0)
)
.subscribe(x => console.log(x))

The example code above is from from RxJS’s own introduction.

We can see that there are two main differences to the “traditional” RxJS 5:

  1. Operators are imported by name from new package called rxjs/operators. Before they were patched automatically on Observable.prototype with import 'rxjs/add/operator/<operator name>'.
  2. We now use a new method called pipe that takes varying number of these operators as parameter and then applies them one by one. Pre-5.5 they were chained as method calls. This was possible since they were found from the prototype of the Observable.

Let’s first discuss the point number one. The way the operators were imported pre-5.5 was cumbersome as there was no automatic importing available. Also the error messages for missing imports were hard to interpret. The 5.5 way provides a better developer experience by allowing code completion with automatic import generation as the operators are explicitly imported from the package. It also allows more optimizations as it is easy for the optimization tools to drop out the unused operators in a process called tree-shaking. This was also somewhat possible with pre-5.5 way to import but not all optimization tools supported it this way and it was really easy to forget to remove the unused imports as it wasn’t noticed as unused by linters and compilers. Also, each import was global as the imports patched the global Observable which lead to a confusing situation where the import was only required for the very first usage of an operator in the project and after that it was available in other files too. This also applied for all of the usages in libraries used by the application.

The second difference makes the code a little more verbose by enforcing the usage of yet another method (pipe) to apply the operators. It may not seem too verbose but it can still add up for example in a case where you have multiple functions chained with each applying a bunch of new operators to Observable before returning it. Now on each function you need to first call the pipe and then pass operators to it as parameters. Maybe not the most verbose thing in the world but still noticeable difference in usage as it comes up all the time.

The Benefits of Lettable Operators

We already covered three of the four main reasons to use the lettable operators described in the RxJS’s own write-up. These were augmentation of Observable.prototype (first point on post), missing compatibility with tree-shaking (point two) and missing support for linters and build tooling (point three).

The fourth point is about the easiness of creating custom operators. Even though declaring your own operators has been possible since the release of RxJS 5, it hasn’t been so easy. RxJS 5.5 improves on this as the methods are now just functions which return a new function with signature of ((source: Observable<T>): Observable<R>). This approach is called function composition and it is the essential idiom behind lettable operators and their benefits. If you wonder why an operator is a function returning a function, just think about how the operators are consumed like for example map(x => x * x) and take(5). Let’s take an example of a simple custom operator that simply uses the existing map to implement toPower operator that raises the number in stream to power of the parameter n.

import { Observable } from 'rxjs/Observable';
import { map } from 'rxjs/operators';

export const toPower =
  (n: number) =>
    (source: Observable<number>): Observable<number> =>
      source.pipe(map((x: number) => Math.pow(x, n)));

which could then be used as

import { range } from 'rxjs/observable/range';
import { toPower } from './to-power.operator';

range(0, 10).pipe(toPower(3)).subscribe(x => console.log(x))

The benefits gained from lettable operators are – without a doubt – huge. It improves the tooling, bundle sizes and developer experience without any obvious drawbacks.

So What Is Wrong With Adding the Lettable Operators?

So based on the beginning of the post the lettable operators are simply awesome. The problem isn’t actually the implementation and features of lettable operators but instead on the hardness of wrapping your head around reactive programming in the first place. I know this first-hand based on the numerous trainings I’ve held that have included a lesson on reactive programming with RxJS.

Reactive programming is one of the hottest topics in the industry at the moment. Yet, I rarely face a candidate in a job interview that has actual experience with reactive programming. Only a few more even know what it means. It is a shame but I’m convinced this will change in the upcoming years as so many important players in the field are bringing reactive programming as an essential part of the solutions they are providing. EcmaScript proposal about Observables (currently ready to move to stage 2 out of 4 stages before reaching the standard) is a great example even though it will take years to make it through to the published standard. Also very significant frameworks, like Spring 5 WebFlux (Reactor) and Angular (RxJS) to name a few, have adopted reactive programming as the core of their implementations.

So why hasn’t RxJS gained as much traction as it should have? I think the main reason is that it simply has been overally too complex to use. I’ve seen this first-hand in multiple projects where it has been constant frustration for programmers not used to it. There are multiple points that have made it hard for beginners to get started:

  • RxJS 4 vs. 5: Still today googling brings up results for version 4 instead of the 5 and they are hard to differ from each other as they share the very same terminology.
  • Import format for operators as discussed above.
  • Poor documentation: Check out for example the documentation pages for the first subjects presented in the docs – AsyncSubject & BehaviorSubject. Also the documentation itself seems not to be so search engine friendly. For example googling rxjs timer doesn’t even show the link to corresponding method in RxJS 5 documentation. Instead it brings up the RxJS 4 documentation as a first result. Thankfully the RxJS 4 documentation was added a link to point to the RxJS 5 documentation on top of the page some time ago.
  • Cold vs. hot observables: Everything in JS world has always been hot. RxJS observables being always cold is a constant source of confusion for newcomers.
  • Completability and cancelling: Subscriptions can be cancelled (unsubscribed) unlike promises and callbacks. It is a great feature but it adds up on the burden.

The list is not comprehensive but underlines the fact that RxJS already contains enough confusing elements. Adding a second syntax for such an essential parts like operator applying and imports makes it even harder to adopt by newcomers. For this reason I don’t think it is/was a good idea to release these changes as 5.5 and as an alternative syntax.

What Should’ve Been Done Instead?

Not meaning to pick on the authors of RxJS at all (they’ve done outstanding job on RxJS 5 and I really appreciate it) but maybe this could’ve been thought once designing the library API in the first place as the JS world wasn’t that different when releasing RxJS 5.0.0 less than one year ago. Well, easy to say now and cannot be changed anymore.

I think a better way to implement this kind of major change would’ve been to release RxJS 6 with deprecation or removal of the old syntax and imports. I’m not quite sure whether deprecation or removal would’ve been the correct choice as they both have their benefits and disadventages.

Deprecation would left the confusing syntax to exist and be found all around. Yet, it would give a lot of time for developers to slowly migrate out of the old syntax with meaningful warnings shown for the usage of deprecated way.

Removal of the old syntax would mark a clear difference on the RxJS 5 and 6 thus being clear what is needed to move to RxJS 6. After all, the changes required are straightforward and easy to apply.

Conclusions

Lettable operators are here to stay and they are a great improvement on the developer experience of RxJS 5. Yet, they add even more cognitive burden to newcomers trying to absorb the reactive paradigm and RxJS 5. Thus, I would’ve loved to see them only brought in as a new major version (RxJS 6) with deprecation or removal of the earlier syntax.

Who knows if this could still be affected. RxJS 5.5 is still in beta 0 after all. Let me know what you think about the topic on comments below!

Leave a Reply

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

Join the team

DevOps Specialist

Madrid, Helsinki, Tampere

Backend Developer

Helsinki, Tampere

Frontend Developer

Helsinki, Tampere