YouTube video for this article
Signals and fine-grained reactivity are very popular right now. In the last month, both Preact and Qwik announced support for fine-grained reactivity with signals. Previously, both frameworks had React-like rendering strategies. Now, they will have a rendering strategy more similar to SolidJS, with signals feeding precise updates into the DOM, with no need for components to rerender. This is what enables SolidJS to have performance almost as fast as imperative vanilla JS.
Reactivity is for code organization
However, if you're used to React, it's easy to focus too much on the performance aspect, and miss the real point of reactivity: better code organization. We could always directly update DOM nodes in event handlers if we were just trying to achieve optimal performance. But reactivity increases developer productivity by allowing code to be declarative and easier to understand.
Reactivity enables a thing to declare its own behavior, instead of being controlled by other code scattered across callbacks or handlers. It's a better separation of concerns. Hence the famous React hook examples with code highlighted according to the feature it managed, and hooks (which are reactive) grouping the same colors together.
This could be better. If the useState
and useEffect
were abstracted into a custom hook, the component could be completely declarative.
Synchronization is not enough
Everyone is excited about synchronization, because it's the most common reactivity, and easiest to implement. But async reactivity provides just as much benefit to code quality as sync reactivity. If you're familiar with RxJS or TanStack Query, you know what I'm talking about. Here's an example from an article I wrote for Angular:
Asynchronous reactivity is difficult to support
So if asynchronous reactivity enables this awesome separation of concerns, why are people saying signals make RxJS unnecessary?
The problem is, async reactivity is complex to implement. So you're not going to see a primitive show up that handles all asynchronous behavior without extra utilities created on top. Basically, synchronous reactivity is the map
operator from RxJS. That's easy. That's like what Solid has, and Svelte, and what Angular is working on, etc. But async reactivity is ALL the other 113 operators of RxJS. So, async reactivity improves code organization just as much as synchronous reactivity, but a lot more work needs to be done to support it.
Out of the box, you can do this in SolidJS:
const x = createSignal(1);
const y = () => x() * 2;
That's easy. But how do you declare that y
is x
but debounced by 2 seconds? That requires a setTimeout
and an imperative update, so y
is no longer declarative. To make it declarative, you have to abstract that behavior into a useDebounce
hook. That takes work.
RxJS is a great temporary solution for any framework
There are 114 RxJS operators. Yes, most of them are rarely useful. But I've found situations perfect for 30+ of them. If you want declarative code and you need to create an async feature today, you need something like RxJS to handle the async stuff.
In the future, it will probably be different. You can re-implement everything in RxJS using hooks for each framework and most likely come up with something more efficient.
But for now, RxJS is extremely useful. It already has many async utilities that not even a mature framework like React has. As you write declaratively with RxJS, you structure your code in a way that will be easy to refactor into a custom hooks solution later on.
Current state of RxJS compatibility in frameworks
So. RxJS compatibility is huge for developers who have already learned to appreciate declarative code. That's why I keep talking about it, and that's why I am working on a proof of concept for Qwik.
All the frameworks have various levels of support for RxJS. This isn't based on any objective data, just what my subjective experience of using observables with these frameworks has been:
Svelte
Apparently RxJS compatibility in Svelte was mostly a happy accident, since Svelte stores have an API almost identical to observables.
SolidJS
SolidJS is quite good, with utilities written in SolidJS itself to support using RxJS with signals. However, when converting observables into signals, a subscription is immediately created, which slightly reduces the power of RxJS. But it seems like Ryan Carniato is revisiting this.
Angular
Angular's support is the most disappointing to me personally. I have the most experience with it. The best thing Angular itself provides is an async pipe, which doesn't take advantage of RxJS's potential for fine-grained reactivity, simply flagging the component as requiring change detection when a value arrives at the template. And the syntax of the async pipe is awkward. I recommend using RxAngular for both better performance and syntax.
On the positive side, the overall Angular ecosystem is very RxJS-friendly, which is awesome.
However, despite Angular developers already being familiar with RxJS, the Angular team is working on a new, less capable reactive primitive. It's basically going to do the job of the map
RxJS operator. Why would they do this? My guess is that Angular is feeling insecure about its past few years being rated poorly in developer satisfaction, and that the team sees RxJS itself as a culprit, rather than Angular's poor integration with it. But some of the most popular and longest-ignored issues on GitHub were for better RxJS integration.
Many Angular developers in fact dislike RxJS, and it seems as though the Angular team thinks they dislike it for its specific syntax, and not for the reasons I believe, that 1. the integration with every aspect of Angular it touches sucks, and 2. they don't want to override their imperative coding habits.
Whatever the reason, I do not believe RxJS's syntax is the problem.
Are these radically different?
// Potential new reactive primitive
const [x, setX] = createState(1);
const y = computed(() => $(x) * 2);
// RxJS map operator
const x$ = new BehaviorSubject(1);
const y$ = x$.pipe(map(x => x * 2));
Do you think a developer would love one and hate the other? I don't.
Angular has had the best opportunity to take advantage of RxJS, and has dropped the ball the hardest. I'm afraid Angular is going to wander through a reactivity desert for a long time before it finally gets around to improving its RxJS integration.
I'm sorry I sound so bitter in this section, but I and many others feel like we waited years and were promised a better RxJS experience only for Angular to now turn its back on one of its only uniquely good parts. RxJS is actually part of Angular, and it feels like they're going to abandon it with as little care as they put into including it in the first place. I hope I'm wrong.
And for some reason, the Angular team is currently "inspired by" Svelte stores, which are basically a less-capable version of BehaviorSubject
. Instead, they should be inspired by the fact that Svelte accidentally has better support for a library that Angular's core depends on than Angular does.
Edit: I changed my mind about this. RxJS has some limitations I forgot about. Read more here.
React
React has the most declarative async alternatives to RxJS, so while RxJS in React isn't a great experience, it isn't needed as much either. TanStack Query and other custom hook libraries go a long way in React. It would still be nice to have a single library with custom hook patterns as well thought out as RxJS. Hopefully TanStack Query will gradually evolve into something as thorough and consistent as RxJS, but it's already pretty awesome.
Qwik
Qwik is an amazing framework, but it has virtually no compatibility with RxJS, since RxJS can't be serialized.
I'll be writing an article and posting a video about my Qwik + RxJS integration soon. I'm on my 2nd iteration, and it's complicated. I'm putting subscription data into Qwik stores so it can be serialized. It seems like it's going to work. It's some runtime overhead that might make it slightly less efficient. But, it allows you to organize your code declaratively, so when a more efficient solution is available, minimal refactoring is required.
I love Qwik, but I'm never returning to an imperative coding style. For most websites, spaghetti code is far worse for business than a few kilobytes of JavaScript.
I want the best of both worlds.
Conclusion
RxJS rocks. Use it.
Top comments (14)
React hooks, Vue reactivity, Svelte stores, SolidJS signals, all of these pale in comparison to RxJS. But I think for most people RxJS just goes over their head.
map
is about the only thing they can understand. Then its back to writing imperative code, subscribing to observables imperatively, nesting subscriptions etc. I think signals are deceptively simple at first glance but after a while you end up with a hundred "use" functions just to do what rxjs does out of the box.It really is disappointing to hear that Angular team sees RxJS as something of an appendage and not a core feature of Angular.
Exactly my point. The R.O.I of learning RxJs is very high for long term. Maintaining asyncronous code is considered hard but after having fundamental knowledge of RxJs it is like a cake. RxJs is very underrated and it can be not only used on FE but also on BE such as RxJava which is quite popular too. If people are lazy to invest 2-3 weeks for RxJs fundamentals then what type of developers they are.
I spent 60 hours in a week in 2017 diving deep into RxJS, and reactivity basically clicked for me after that. Since then I've been frustrated with other people's resistance with it. However, I think it's more productive to frame it as a change in habits that's required. Easily changing habits is a good skill, but there are some good developers who have never dived into RxJS to appreciate the declarative mindset. So I don't automatically think devs who hate RxJS are bad developers. I just think they haven't seen a use case that convinced them to give it enough effort, and when they do (and they will eventually), they will be able to change their mindset.
I had done the same, spent 10s/100s of hours learning RxJS, playing with it, testing it, fighting with it etc. It made sense afterwards and I loved it but there aren't many people who code/learn in their spare time.
Most people do a "hello-world" angular tutorial, search for a job and work 9-6. As soon as it hits 6 they stop working, stop coding and stop learning and the only time they progress is what they read on their job.
I know it's frustrating that people aren't as passionate but most people won't spend 60 hours learning RxJS and then you just have a codebase that alienates everyone (at least the complex chains). The subscribes in subscribes never stop appearing in PRs and I'll continuously suggest how to do it properly.
My biggest difficulty when learning RxJS was that you basically have to transform everything into async (which makes sense) but I can only imagine how hard that is for everyone else. I spent way too much time trying to avoid BehaviorSubjects because it's bad practise and now everything is one, because it's 100x easier for me and everyone else when I can just push values in and get them out syncronously.
Maybe it's a paradigm worth teaching the next generation of programmers, so they at least enter the job market with it.
RxJS makes it easy to describe complexity in code. That's a trap, don't fall for it. You'll be easily misguided into adding complexity that is not even necessary. There might be cases complex enough to warrant the use of RxJS, but many of the cases that seem like they do are merely not understood well enough to resolve the complexity outside of the code.
You could say that about JavaScript. It's not the point of RxJS though. I hope you read the article before writing this comment.
Edit: I am in favor of avoiding unnecessary complexity though.
It's a bit like with RegExp: if you learn to master RegExp, you tend to overuse it, because they are so powerful. The same is true for RxJS. Once you finally understood it, you might be prone to overuse it, even if the problem does not warrant its use. I really hope that I can do without RxJS most of the time, because its usage implies a higher level of complexity than might be necessary, or worse, that is warranted (and then, I might be solving the wrong problem).
I don't care what the complexity is. That doesn't matter. Reactive code is cleaner than imperative code. I'll use RxJS if something needs to be dynamic and synchronous reactivity isn't enough for it and there are no framework-native declarative solutions (like a debounce hook).
If you still think RxJS is about complexity, give me an example of what you're talking about where RxJS makes something inherently more complex than an alternative.
I should have been more clear. RxJS is obviously about creating, managing and consuming async data - but, and that's my point - allows to easily encode complexity into your solutions using it.
If a problem is too complex to be solved with framwrok-native or tried and tested primitives, it is maybe too complex and needs to be simplified instead of throwing RxJS at it without prior consideration. That's all I meant.
Okay. I haven't had that problem.
Thanks Mike, I've read this article right after "I changed my mind. Angular needs a reactive primitive" one, and I like this one a lot more.
I understand everything you write in that one, but I'm still not convinced those problems can't be solved by extending over RxJS, even adding more "primitives" to it along with Observables, Subjects, BehaviorSubjects etc.
I wonder what RxJS team thinks about this "trend".
Very interesting; thanks for sharing. What's the references that the chart of compatibilites is based on? Your own tests against the 114 operators?
It isn't based on any objective data, just what my subjective experience of using observables with these frameworks has been. I'm interested if people will disagree, but I don't think they will, except maybe putting SolidJS above Svelte for some frustrating corner case in Svelte that I wasn't aware about because I have the least experience in Svelte.