RH
Article
(01)

RxJS: When Elegance Becomes an Unnecessary Maze

A personal reflection on overusing RxJS in Angular and why simplicity and Signals are winning the battle against complex data streams.

By
435 Views
RxJSAngularReactive ProgrammingWeb DevelopmentClean Code

That phase where everything was a Stream

If you've been in the Angular ecosystem for a while, you know exactly what I'm talking about. There was a time, not so long ago, when we drank the reactive programming Kool-Aid by the bucketload. I was the first one walking into the office saying that if something wasn't an observable, it was badly designed. I perfectly remember that feeling of power I got from chaining operators: a switchMap here, a combineLatest there, and boom!, magic happened. I felt like an orchestra conductor managing asynchronous data streams with an elegance that, honestly, made me feel like a better developer than I probably was.

But of course, over time you realize that elegance comes at a price, and sometimes it's a steep one. I've spent entire nights trying to debug why a data stream wasn't firing, only to find a shareReplay(1) acting up in a corner I'd forgotten about. What started as an attempt to make more robust apps turned into a labyrinth of reactive logic where you even had to ask three services for permission just to toggle a boolean and then subscribe in the component with that damn async pipe.


My experience banging my head against the code

There was one project in particular that opened my eyes. It was a pretty heavy management app with lots of dynamic forms and shared state. I put on my RxJS superhero cape and decided that every piece of app state was going to be reactive. I wanted to avoid mutations at all costs. The result was code that was technically "pure" and followed reactive principles to the letter, but it was practically unreadable for anyone who wasn't me (and even for me, three months later).

I realized we were spending more time fighting RxJS operators than actually implementing features. Every time a junior joined the team, onboarding was a nightmare. We had to explain that to understand how a simple error message appeared, they had to trace a Subject that went through five transformations before reaching the view. That's when I started asking myself: Do we really need all this for an app that, at the end of the day, just shows data from a database?


Why I think simplifying is important

Cognitive complexity is the silent enemy of software development. When we use RxJS for absolutely everything, we're forcing our brains (and our teammates') to maintain a mental model of temporal flows that's very hard to process. Reading a variable assignment isn't the same as understanding when and why a value will emit in an async stream that depends on three other streams.

Honestly, I think we got carried away by the "total reactivity" trend and forgot that the tool should adapt to the problem, not the other way around. RxJS is incredible for managing complex DOM events, websockets, or concurrent HTTP requests. It's a Swiss Army knife for asynchrony. But using it to store whether a modal is open or closed... come on, let's be honest, it's like using a sledgehammer to crack a nut.


What I've learned (the hard way)

With the arrival of Signals in Angular 17 and 18, my view on state management changed completely. I realized that a lot of what I was doing with BehaviorSubjects could be solved in a much cleaner, synchronous, and predictable way. I've learned to be much more pragmatic. Now, my golden rule is: if there isn't a real need to manage time or multiple asynchronous emissions, don't touch RxJS.

I've gone back to enjoying the simplicity of functions that return values, variables that change clearly, and side effects that are easy to track. Here's a small example of what I used to do versus how I approach it now when the flow doesn't really require reactive power:

// Before: The observable labyrinth
nombreUsuario$ = user$.pipe(
  map(u => u.name),
  startWith('Guest'),
  distinctUntilChanged()
);

// Now (with Signals or simple logic):
nombreUsuario = computed(() => this.user()?.name ?? 'Guest');

The difference isn't just syntactic; it's about intention. The second option is much easier to reason about, doesn't require manual unsubscriptions (we always forget one), and doesn't carry that mental load of "when will this emit?".


Final thoughts

Don't get me wrong, I still love RxJS. I think it's one of the most powerful and well-built libraries in the JavaScript ecosystem. But my relationship with it has matured. It's no longer that blind infatuation where I wanted to take it on every date. Now I save it for special occasions where it actually adds value.

If you're starting a project or find yourself refactoring an old one, here's some advice: don't feel less professional for using a simple variable or a Signal instead of a complex stream. On the contrary, true mastery is knowing how to choose the simplest tool that solves the problem effectively. Your future self, when they have to go back to that code six months from now to fix a bug on a Friday at 5 PM, will thank you forever. Less is more, truly. Sometimes, the most elegant architecture is the one you barely notice.

© 2026
Roberto Hernando
|