DEV Community

loading...
Cover image for 🙅‍♂️ Stop trying to learn RxJS

🙅‍♂️ Stop trying to learn RxJS

richytong profile image Richard Tong Updated on ・5 min read

If you have any interest in RxJS, this article is for you. If you have no idea what RxJS is, I advise you not to worry about it and check out rubico: a new asynchronous functional programming library authored by yours truly.

At the highest level, the relationship between rubico and RxJS is characterized by convergent evolution, that is

the independent evolution of similar features in species of different periods or epochs in time

Emphasis on different periods in time. One period without async iterables, and one period in the future when they're a thing.

For the longest time, I had at best a hazy idea of RxJS. In fact, My first encounter with the reactive paradigm was through talks of RxSwift with a pal of mine from an old job. It wasn't until after I had heard numerous comparisons of RxJS to rubico that I really hunkered down and dove in.

This set me off to learn RxJS, and I'll be honest, I was disappointed. At no point in either of the release branches or the main site do they talk about why RxJS. It always just starts with what. It's like reading a research paper that starts with "hey guys, here's our research! it's important because we say so!". There needs to be a reason why the research is being done, otherwise no one will care. Just take a look at any TC39 proposal, it always starts with why. For RxJS, on the other hand, the current first time experience on the site is

  1. Introduction - "RxJS is..." <- this is what
  2. Example 1 - RxJS is powerful because you can use pure functions and pure functions are less error prone
  3. Example 2 - RxJS has operators for flow control
  4. Example 3 - RxJS has operators to transform values in RxJS observables
  5. Observables - "Observables are..."

They never answered my first question: why is this here in the first place? On rubico it's

  1. Motivation - Why is this here? Because When I was still writing in the imperative style, I got tired of looking at my own code. I created this library for myself, so I can write in a style I love.
  2. Principles - What is the highest level of what I want from this library? Simple code; don't care about async; and simple, composable, and performant transformations (on all collections)
  3. rubico follows these principles to grant you freedom
  4. Introduction - take the tour, read the docs
  5. Examples...

Why should always precede what. Otherwise, your product will run in circles. I experienced this first hand at my previous company.

battle of chattanooga

From the creator of RxJS

RxJS isn't easy to learn. But the alternative lessons you'll learn if you try to roll your own streaming paradigm for your app with a lot of streaming data will be much, much more painful and costly.

I mean I can't predict the future, but I agree. I trust that the creator of RxJS speaks from experience. There will probably be a lot of pain rolling my own streaming paradigm, which is why I'm not going to roll my own streaming paradigm. In fact, I'm not going to invent another paradigm at all; I'm just going to borrow the existing functional paradigm and polish it for JavaScript.

...and at the end of the day, anything you create will just be another flavor of Observable without the safety guarantees.

Does the async iterable count as another flavor of the Observable? It seems the spec is leaving RxJS behind when async iterables are already here to stay, while Observables are a stage 1 proposal. Observables are not async iterables, but they compete for the same streaming data model. Async Iterables were created partly because of the pains of NodeJS streams, which are analogs to RxJS Observables. Part of these pains are backpressure problems, from RxJS creator:

Since observables are all push-based, there are memory and back pressure concerns unless you use a lossy strategy to adapt the incoming values into the asyncIterator. I have a proposal to add this as a feature to RxJS, but it's not without it's drawbacks.

^ that turned into this library.

You can see excitement for async iterable streams in this issue in the streams spec. Read it for the good vibes. Here's a nice summary from domenic

It's starting to feel a bit magic though....

Really, I'm just against spaghetti code. You'll get spaghetti if you model streams as a push datatype like Observable. You'll get worry free code if you model streams as async iterables. Finally, you'll get magical code if you feed an async iterable to rubico's transform. Check out this functional style async iterable webserver with rubico and deno

const s = serve({ port: 8001 });
console.log("http://localhost:8001/");
transform(map(req => {
  req.respond({ body: "Hello World\n" });
}), null)(s);
Enter fullscreen mode Exit fullscreen mode

Here's my abridged list of features and roadblocks comparing RxJS and rubico. I encourage you to voice any additions or subtractions in the comments.

Features of RxJS Features of rubico
special asynchronous stream datatype, the Observable - wraps built-in types for reactive operations down the line no special data types: rubico works out of the box for built-in types; this includes async iterables
familiar design pattern: the Observer pattern - plugs into Observables no design pattern lock-in: rubico composes your functions together. You choose the design pattern
interop with Promises fully managed Promises: rubico stops you from having to manually resolve Promises, for example having to call
Promise.all on an array of Promises
functional programming style via Operators + Observable.prototype.pipe functional programming style by design
Roadblocks for RxJS More Features of rubico
async iterables undermine the need for rxjs's core datatype: the Observable. async iterables make rubico great; the async iterable is a core rubico (and JavaScript) datatype
multitude of concepts simple foundational concept: just pipe functions together
large API surface small, cohesive API surface
RxJS is hard to learn rubico is easy to learn

I'm telling you to stop learning and using RxJS because rubico is that much better at doing what RxJS is trying to do. Don't get me wrong, rubico is easy to learn, but it takes time and hard work to master. I urge you to put your time to better use: take the tour.

Discussion (14)

pic
Editor guide
Collapse
anduser96 profile image
Andrei Gatej

Have you tried reading RxJS’ source code? I bet you’ll learn a lot more than you’d do with diagrams. Even more, it won’t seem that difficult!

Collapse
richytong profile image
Richard Tong Author

is there a particular spot in RxJS' source code you found to be helpful for learning RxJS?

Collapse
anduser96 profile image
Andrei Gatej

I’d say everything. It’s all about linked lists and OOP concepts. It may seem a lot, but it’s the same pattern used almost across the entire library.

Thread Thread
richytong profile image
Richard Tong Author

Okay, I'll do that, thanks.

Thread Thread
anduser96 profile image
Andrei Gatej

Awesome! I’m sure it will be a fun journey.

I’d recommend creating an rxjs project in StackBlitz and try to debug the sources right there. With stackblitz you can debug the TS files.

I’d be glad to help if you’ll have questions! Good luck

Thread Thread
richytong profile image
Richard Tong Author

I have a couple questions, Andrei. How did you get into RxJS? How has it helped you in your work?

Thread Thread
anduser96 profile image
Andrei Gatej

About one year ago I started learning Angular, that’s when I got started with rxjs as well. A couple of months ago I started digging through its source code out of curiosity and out of a little frustration that the diagrams weren’t enough for me, and I always felt like something was hidden from me.

I couldn’t say it helped in my job, because I stopped working due to some incoming exams, but I’ve been training myself on Stack Overflow. Now that I’m more familiar with how it really works, I stopped thinking about “data over time” and other analogies. For me, thinking about how the entities are connected with each other its enough. Of course, I cannot remember every detail, but my go-to documentation is the source code itself.

I feel like it helped me a lot to solve problems(e.g those from SO), which I couldn’t have been able to solve without knowing a bit of how the library really works.

Thread Thread
richytong profile image
Richard Tong Author

I agree with your approach to go to the source code directly. And I really like the mental model for Observables in the context of Angular, it's like a perfect counterpart for the event-driven architecture of Node. I can't agree more, the source code should speak for itself.

I've been training on stack overflow too, actually I put the code for rubico up a couple times and got it roasted by the internet giants.

I guess in the Observable sense, there's no competition actually. In rxjs v7 Observables will be async iterable, and that actually ties in really nicely to rubico's transform. I made an issue here, but it looks like I won't have to do any extra work on it, unless I want to have backsupport for v6.

I do have a problem with the operators though. rubico and rxjs can coexist, but I do not like the API with the operators. I am honestly competing rubico there because I think rubico can do it more concisely, more functionally, and more readably. I would like to have your thoughts on this.

Thread Thread
anduser96 profile image
Andrei Gatej

Could you elaborate on rubico can do it more concisely, more functionally, and more readably. ?

I personally have nothing to complain about rxjs, it's my favorite library to be honest.

  • concisely - what's the rxjs' flaw in this case?
  • functionally - I doubt it. rxjs has a lot of functionalities and some of them are extremely well-thought; it also offers enough tools to create marble tests
  • readably - sometimes this is subjective, but I don't find rxjs code unreadable at all

In addition to this, rxjs is written in TS, which is a very big plus.

I personally think that rubico is a nice side-project and if you can develop something like this on your own(700+ commits), then reading rxjs' source code and understanding it will be a fun journey.

IMHO, there are also other parts that factor in whether people should use rubico instead of rxjs. rxjs is also used by other important libraries, such as nest.js, ngrx.

I believe it's pretty hard to convince people to learn rubico, as there is already a tool like rxjs which solves a lot problems and has a lot of functionalities that have been built over time. My opinion is that you should jump on the rxjs train!

Thread Thread
richytong profile image
Richard Tong Author • Edited

concisely - rxjs's fatal flaw in this regard is the Observable's influence over the rxjs/operators api. For example, rubico has only one function map while rxjs has concatMap, exhaustMap, flatMap, mergeMap, and switchMap to name a few. The difference between these map functions is in the behavior on the underlying Observable. rubico does not have the issue of having to deal with an underlying Observable; it just projects each element of whatever collection is passed to it onto another collection of the same type. In this regard, rubico is more concise because it defines predictable behavior for a multitude of types (Array, Set, Map, ...) including the observable (v7 with @@asyncIterator).

functionally - I meant functionally as in the functional style, for example

Observable.of(1, 2, 3)
  .pipe(
    filter(x => x % 2 === 1),
    map(x => x + x),
  )
  .subscribe(x => console.log(x))

is more object oriented via .pipe and .subscribe than the pure functional approach with rubico

pipe([
  filter(x => x % 2 === 1),
  map(x => x + x),
  map(x => console.log(x)),
])([1, 2, 3])

Notice how with rubico, there are only functions except for the last input array. The power behind rubico is that the input is last instead of first. This is powerful because it doesn't lock you in to one type of collection; you have the freedom to transform any collection with rubico (including the v7 observable).

readably - I know I sorta dug myself into the subjective hole with this one, but please hear me out. If we consider readable from the perspective of how many more people can read your code, rubico code will lead to less questions because there's less magic happening. rubico's functional purity and no special data types means you know exactly what is going to happen to your array if you map over it. In fact, please take a look at the implementation for map; there's really not a lot to it. It's just map that works on a lot of the built-in types. rubico also did not invent it's own paradigm, it just follows the functional one. In that regard, much of rubico's API naming is borrowed from the JavaScript language, popular JavaScript libraries, and even other languages. For example, RxJS, ramda, rubico, bash, this proposal, and scala all have the concept of a pipe whereby you chain functions together. Every single one of rubico's method names are derived in this way. Why? Because I wanted to make the code you write with rubico look as familiar and intuitive as possible. I cannot say the same for RxJS when they export functions in their 'rxjs/operators' API with names like bufferWhen, mapTo, switchMap, switchMapTo, etc. This API is questionable, to say the least, especially when there are whole articles dedicated to distinguishing the differences between some of these names.

It's been an uphill battle to push out rubico for exactly the reasons you mention: RxJS has an entire ecosystem written around it and is way older and more mature. I think the difference here is rubico avoids many of the issues of RxJS by design. For example, you can consume async iterables with rubico's transform, but async iterables right now are causing problems for RxJS because they undermine so much of the functionality of Observables. From an API consumer's perspective (aka you and me), async iterables and observables are effectively the same; just attach a function to a stream of events

Observable.of(1, 2, 3)
  .pipe(
    filter(x => x % 2 === 1),
    map(x => x + x),
  )
  .subscribe(x => console.log(x)) // RxJS

const myAsyncIterable = async function*() { yield 1; yield 2; yield 3; }

transform(pipe([
  filter(x => x % 2 === 1),
  map(x => x + x),
  map(x => console.log(x)),
]), [])(myAsyncIterable()) // rubico

This problem with async iterables has motivated ben to publish the library in his tweet that provides strategies for converting Observables into async iterables. More details in this pr and this issue. If you ask me, RxJS is a sinking ship. Please come over to rubico.

Thread Thread
anduser96 profile image
Andrei Gatej

rxjs's fatal flaw in this regard is the Observable's influence over the rxjs/operators api

An operator is a function which returns a function whose single argument is an Observable and whose return type is also an Observable. So, the Observable is just a fundamental unit of the library. If you'd spend some time reading the sources, you should see that this akin to that Node class which you use to build a linked list.

For example, rubico has only one function map while rxjs has concatMap, exhaustMap, flatMap, mergeMap, and switchMap to name a few.

These operators surely exist with a purpose. I find it very nice that rxjs has these options, because they are frequently met when solving problems.

it just projects each element of whatever collection is passed to it onto another collection of the same type.

rjxs' map might be the same as rubico's map, but mergeMap is very useful when you're dealing with another observable(inner observable) and you need that value to be passed along in the stream.

In this regard, rubico is more concise because it defines predictable behavior for a multitude of types (Array, Set, Map, ...) including the observable (v7 with @@asyncIterator).

This makes me think that you didn't not carefully read on what observables are and what problems do they solve.

functionally - I meant functionally as in the functional style, for example

I find this more intuitiveL

of(1,2,3).pipe(a(), b()).subscribe()

than this:

pipe([
 a(),
 b(),
 c(),
])([1, 2, 3])

because in the first snippet you can clearly understand which is the source and which is the consumer.

rubico code will lead to less questions because there's less magic happening.

IMHO, less magic is not an advantage. As long as the magic is abstracted away from the dev, and it magically works, I see no problem in this.

I cannot say the same for RxJS when they export functions in their 'rxjs/operators' API with names like ...

I'd that these operators exist with a purpose. Yes, at first bufferWhen, buffer and bufferToggle might seem not intuitive, but after understanding their purpose, everything should make sense. Not to mention how many benefits you'd get out of reading their source code.

but async iterables right now are causing problems ...

Here's an article which describes a possible use case for them(testing). I don't see a breaking change, it's just another feature.

If you ask me, RxJS is a sinking ship

I would disagree since it's used by important technologies, such as angular, ngrx etc...
I think you would benefit a lot if you started contributing to rxjs(by writing articles, SO, helping other people). I just see no point in going against this with another library, because rxjs is really great. It also has a lot more features: testing your own observables using marble tests, multicasting, adding teardown logic.

Thread Thread
richytong profile image
Richard Tong Author • Edited

Andrei, I oppose rxjs because the ideology is weak. At no point in either of the release branches or the main site do they talk about why RxJS. It always just starts with what. It's like reading a research paper that starts with "hey guys, here's our research! it's important because we say so!". There needs to be a reason why the research is being done, otherwise no one will care. Just take a look at any TC39 proposal, it always starts with why. For RxJS, on the other hand, the current first time experience on the site is

  1. Introduction - "RxJS is..." <- this is what
  2. Example 1 - RxJS is powerful because you can use pure functions and pure functions are less error prone
  3. Example 2 - RxJS has operators for flow control
  4. Example 3 - RxJS has operators to transform values in RxJS observables
  5. Observables - "Observables are..."

They never answered my first question: why is this here in the first place? On rubico it's

  1. Motivation - Why is this here? Because When I was still writing in the imperative style, I got tired of looking at my own code. I created this library for myself, so I can write in a style I love.
  2. Principles - What is the highest level of what I want from this library? Simple code; don't care about async; and simple, composable, and performant transformations (on all collections)
  3. rubico follows these principles to grant you freedom
  4. Introduction - take the tour, read the docs
  5. Examples...

Why should always precede what. Otherwise, your product will run in circles. I experienced this first hand at my previous company.

RxJS is giving me PTSD because all I see is what. RxJS is a sinking ship because the spec is moving on without them: async iterables are here to stay, while Observables are a stage 1 proposal. Observables are not async iterables, but they compete for the same streaming data model. Async Iterables were created partly because of the pains of NodeJS streams, which are analogs to RxJS Observables. Part of these pains are backpressure problems, from RxJS creator:

Since observables are all push-based, there are memory and back pressure concerns unless you use a lossy strategy to adapt the incoming values into the asyncIterator. I have a proposal to add this as a feature to RxJS, but it's not without it's drawbacks.

^ that turned into this library.

You can see excitement for async iterable streams in this issue in the streams spec. Read it for the good vibes. Here's a nice summary from domenic

It's starting to feel a bit magic though....

Really, I'm just against spaghetti code. You'll get spaghetti if you model streams as a push datatype like Observable. You'll get worry free code if you model streams as async iterables. Finally, you'll get /comfy/ code if you feed an async iterable to rubico's transform. Check out this functional style async iterable webserver with rubico and deno

const s = serve({ port: 8001 });
console.log("http://localhost:8001/");
transform(map(req => {
  req.respond({ body: "Hello World\n" });
}), null)(s);
Enter fullscreen mode Exit fullscreen mode

Andrei, I don't think you actually like things that magically work because you are someone who reads source code. Isn't it core to you to understand the way something works by studying its source line by line? You've done that quite a bit for RxJS. I'm not really sure what they did to deserve that. I see your passion, that's why I want you to use and contribute to rubico. Stop wasting your talent on a sinking ship. The project is just me right now, but there's a lot of interesting stuff I've spec'd out. Really, I would love to have your help on this.

Collapse
bjesuiter profile image
Benjamin Jesuiter

Hey @richard Tong,
thanks for this interesting perspective.

I will look into rubico and, if i have time / find something interesting, give you some feedback on this.

I found your post while searching for a state management solution for deno.
I also do have some custom rxjs operators published on npm, so I think, I can nicely compare the two experiences.

Rubico sounds interesting for me, since the idea of 'using the platform' is also my main paradigm right now.
But I also wonder whether rubico will be a real complete (or neraly complete, maybe without edge cases) replacement for the vast functionalities build into rxjs.

Let's see! Anyway, thanks for the post!

Collapse
richytong profile image
Richard Tong Author

Hey Benjamin, thanks for the comment. I think you're right - rubico has a long way to go. Actually I was just on my way to implementing some stuff I have on its roadmap, since today my day is light. Cheers