DEV Community

Cover image for RxJS Deserves Better Than Its Constant Criticism – Lessons From a Real-World Production System
Florent M
Florent M

Posted on

RxJS Deserves Better Than Its Constant Criticism – Lessons From a Real-World Production System

As the Co-founder & CTO of Disign — a robust cloud-based digital signage platform deployed across platforms like Android digital signage players, Raspberry Pi, and many other devices — I want to share why we continue to rely heavily on RxJS, despite the growing trend to move away from it in modern Angular development.

I frequently see developers bashing RxJS as overly complex, outdated, or irrelevant since the arrival of Angular Signals and other reactive paradigms from frameworks like SolidJS or Svelte. But RxJS isn’t just about data binding or UI state, it solves fundamental problems that are critical in real-world, asynchronous, event-driven applications.

At Disign, I’m talking about one of our largest frontend projects, with more than 2,000+ components (although that number can be misleading in terms of scale). What really matters is that we have around 175 distinct business entities modeled on the frontend alone: each with their own logic, state, and reactive requirements. It’s a large and growing codebase, and the architectural decisions we make today affect long-term maintainability and performance. RxJS plays a central role in how we manage that complexity.


RxJS is more than a UI tool

RxJS gets a bad rep because people often evaluate it through the narrow lens of UI change detection or component reactivity. But its true power lies elsewhere: it’s a general-purpose abstraction over streams of values and events, not just DOM updates.

We use RxJS extensively at Disign to orchestrate logic in a player that:

  • Runs 24/7, with strict performance and memory constraints
  • Processes events from many asynchronous sources (touch input, WebSockets, scheduled timers, APIs, sensors)
  • Reacts in real time to clock-based programming and automation
  • Manages user interaction, scheduled content, and live data feeds, all at once
  • Supports third-party applications built using the Disign SDK for interactive signage, which brings additional reactive requirements through embedded apps and custom logic

Why RxJS works for us

RxJS allows us to write deeply flexible and composable business logic. We don’t want our core logic tied to Angular-specific constructs like Signals. Here's an example pattern we use regularly:

yourMethod(): Observable<any> {
  // Do whatever you want. No limits. Sync or async.
  /*
  return combineLatest([
    this.http.get(...),
    this.store.select(...),
    this.socketStream$,
    interval(1000),
  ]).pipe(
    filter(([httpData, storeData, socketData, time]) => ...),
    map(([...]) => this.transform(...)),
    retry({ count: 3 }),
    shareReplay(1)
  );
  */
}
Enter fullscreen mode Exit fullscreen mode

This kind of stream represents business logic pipelines: they’re reactive, easy to test, and adaptable. If requirements change tomorrow (e.g., we need to debounce, delay, switch to polling, etc.), we can update the operator chain without restructuring our whole service.

Compare that with traditional async/await or Promises, they’re great for one-off flows, but don’t scale well when handling concurrent, dynamic, multi-source data.


Signals are cool, but not enough

Angular’s Signals are a welcome addition for template reactivity and fine-grained UI updates, especially in zoneless mode. They make markForCheck() obsolete and remove the need for takeUntilDestroyed() in components.

But Signals fall short when it comes to:

  • Managing external, asynchronous data flows
  • Coordinating logic across services and modules
  • Composing complex behaviors over time
  • Sharing logic outside the component layer

More importantly, they tie your code tightly to Angular. For us, that’s a risk we’re not willing to take. We want our core logic, scheduling, state handling, content orchestration, to be decoupled from any one UI framework.


Async logic is everywhere in Disign

Disign is a very different beast from a typical SPA. While most applications revolve around user interaction, we deal with:

  • Autonomous scheduling of content
  • External triggers from APIs and sensors
  • Live, always-on displays that can't afford memory leaks
  • Tactile interfaces with heavy user input and performance expectations

Here’s what a “simple” scenario might look like:

“At 9:00 AM, if the room is occupied and the temperature exceeds 24°C, switch to the cooling mode scenario, but only if the display has been idle for more than 5 minutes.”

This reads like a single sentence. But under the hood, it requires:

  • Scheduling logic
  • Sensor input streams
  • Display state tracking
  • Debouncing and timing conditions
  • A fallback mechanism if sensors fail
  • An override if a user manually takes control

RxJS handles this elegantly via composition. No imperative timers. No nested callbacks. No unreadable async trees. Just streams.


RxJS and "memory safety"

In a system that runs 24/7, memory leaks are unacceptable. RxJS gives us control over subscriptions, especially when combined with patterns like takeUntil, shareReplay, switchMap, or finalize.

Yes, this comes at the cost of complexity. You need to understand how the stream behaves, how it cleans up, and how operators interact. But that complexity buys us predictability and safety — two traits that matter more in production than "easy to learn."


The native observable spec is coming

Let’s not forget that observables are being standardized for the web platform (WICG Observable Proposal). Just like Promises and async/await became part of JavaScript, Observables may soon follow. When that happens, RxJS will evolve to wrap the native version, just as it did with Promises.

So we’re not betting on an obsolete technology — we’re building on a pattern that has stood the test of time across languages and platforms.


RxJS vs. async/await: A false dichotomy

Here’s the thing: we use both.

  • async/await for imperative, one-shot flows
  • RxJS for declarative, ongoing, multi-source logic

They aren’t mutually exclusive, but for complex systems, RxJS wins every time.


Final thoughts

RxJS isn’t perfect. Nothing is. But it’s one of the most powerful, flexible, and scalable tools in our stack at Disign.

If you’re building:

  • Applications with complex async requirements
  • Systems that run continuously
  • Logic that involves events, scheduling, and user input

Then you owe it to yourself, and your architecture, to take RxJS seriously.


Let’s stop dismissing RxJS because it’s hard. Let’s use it when it makes sense, and in many serious projects, it absolutely does.

And you, do you use RxJS?

I’m always curious to hear how others are using RxJS, or why they’ve decided not to. Do you rely on it in your projects? Have you switched to Signals, or moved away from reactive streams entirely?

  • What has worked for you?
  • What challenges did you face?
  • Are you using RxJS outside Angular: maybe in React, Node.js, or standalone services?

Let’s discuss in the comments 👇

I'm happy to answer questions or dive deeper into our implementation at Disign if it helps!

Top comments (0)