When to use OnPush with signals vs RxJS facades in Angular?

Explore when Angular apps benefit from OnPush with signals or RxJS facades, and how to prevent stale views.
Learn to choose between signals and RxJS streams in Angular, apply OnPush change detection, and avoid leaks or redundant emissions.

answer

Use OnPush with signals for local, component-driven state where fine-grained updates matter (e.g., form fields, UI toggles). Choose RxJS facades/selectors for global, asynchronous, or multi-source data flows. Avoid stale views by immutability and ChangeDetectorRef.markForCheck. Prevent unnecessary emissions with distinct operators or computed signals. Stop memory leaks by using takeUntil, async pipe, or automatic signal cleanup. Match strategy to scope: signals for granular UI, RxJS for orchestration.

Long Answer

Angular now offers two complementary paradigms for reactive state: signal-based state (introduced in Angular 16+) and traditional RxJS streams (facades, selectors, NgRx). Choosing between them depends on scope, complexity, and the nature of data flow.

1) OnPush with signals

OnPush change detection checks only when inputs change or signals emit. Signals are fine-grained reactivity primitives; each holds a value and notifies dependent views when updated. They shine for:

  • Local state (toggled flags, input values, UI mode).
  • Component-driven data where lifecycle = component’s lifetime.
  • Performance-critical UI (forms, dashboards) where updating one field shouldn’t re-render the whole view.
    Signals automatically track dependencies, so templates update when values change—without extra async pipes.
2) RxJS facades and selectors

RxJS streams excel at orchestrating async and multi-source data:

  • Server polling, websockets, or API calls merged with user input.
  • Application-wide state via facades or NgRx stores.
  • Complex operators (debounce, switchMap, mergeMap) for transforming streams.
    Selectors expose Observables consumed in components. With OnPush, the async pipe automatically marks the view dirty when emissions arrive.
3) Avoiding stale views
  • With signals: ensure immutability; reassign objects/arrays (users.set([...users(), newUser])) instead of mutating in place. For OnPush components, Angular’s signal system notifies change detection, but manual triggers (cdr.markForCheck) may be needed when bridging with external events.
  • With RxJS: prevent “lost” updates by combining selectors properly, using shareReplay, and keeping store immutability. Always use async pipe or subscribe with cdr.markForCheck inside OnPush components.

4) Reducing unnecessary emissions
  • Signals: use computed and effect carefully; avoid chaining signals that duplicate work.
  • RxJS: apply distinctUntilChanged, auditTime, or debounceTime to suppress noisy streams. Avoid multiple async pipes on the same selector; derive once and reuse.

5) Preventing memory leaks
  • Signals: they automatically clean up with component destroy, but beware of effect usage—dispose effects manually in services or long-lived contexts.
  • RxJS: leaks occur if subscriptions outlive components. Prevent with async pipe, takeUntil(destroy$), or Angular’s takeUntilDestroyed().

6) Decision matrix
  • Signals: local, fine-grained, synchronous, predictable. Minimal boilerplate. Great for UI reactivity and small-to-medium complexity.
  • RxJS: async orchestration, global state, inter-service data, event streams. Great for data pipelines and larger teams used to NgRx.
  • Hybrid approach is common: RxJS for API streams and global state, signals for component-local rendering.

7) Validation and testing
  • Profile change detection cycles: signals + OnPush reduce CD cost.
  • Test for stale views by updating state and asserting DOM.
  • Track subscription counts to confirm no leaks.

In short: signals are the new default for local reactivity, RxJS remains essential for orchestration. Combine them wisely, with OnPush as the foundation, and always validate with profiling, immutability, and cleanup discipline.

Approach Best For Key Tools Pitfalls Mitigation
OnPush + Signals Local state, fine-grained updates signal, computed, effect Mutating arrays/objects silently Use immutability, reassign values
RxJS Facades/Selectors Global async flows, multi-source data Observable, async pipe, NgRx Unnecessary emissions, leaks distinctUntilChanged, takeUntilDestroyed
Hybrid Most real-world apps Signals in components, RxJS in services Bridging complexity Convert Observables → signals (toSignal)
Avoiding stale views Any OnPush + immutability Missed CD triggers markForCheck, async pipe
Leak prevention Any Auto cleanup + operators Long-lived subscriptions async pipe, destroy hooks

Common Mistakes

Developers often misuse signals by mutating arrays/objects directly, which doesn’t trigger updates. Others misuse RxJS, subscribing manually without cleanup, leading to leaks. Using too many async pipes for the same selector causes duplicate emissions. Some rely on ChangeDetectionStrategy.Default, making performance regressions invisible. Another mistake: forcing everything into RxJS, even simple local toggles that signals handle better. Or conversely, trying to manage global async flows with signals, creating messy bridges. Finally, ignoring immutability causes stale views under OnPush.

Sample Answers (Junior / Mid / Senior)

Junior:

“I’d use OnPush with async pipe for Observables, and signals for simple UI state. I’d avoid mutating arrays directly and use immutability so the view updates.”

Mid:

“I’d choose signals for local toggles and UI state, RxJS facades for global async flows. To avoid stale views, I’d use OnPush + immutability. I’d apply distinctUntilChanged to cut emissions and async pipe or takeUntilDestroyed to prevent leaks.”

Senior:

“I’d architect hybrid: RxJS in facades/services for async orchestration, exposing selectors; signals in components for rendering. OnPush ensures minimal CD. I’d bridge RxJS → signals with toSignal, validate updates with profiling, and enforce immutability to avoid stale views. Memory leaks are prevented with async pipe, Angular destroy hooks, and effect disposal. This balances granular UI performance with scalable global state.”

Evaluation Criteria

Interviewers expect:

  • Clear distinction between signals (local UI reactivity) vs RxJS (async/global orchestration).
  • Understanding of OnPush as baseline for scalable apps.
  • Knowledge of stale view prevention (immutability, markForCheck).
  • Tactics for reducing unnecessary emissions (distinctUntilChanged, computed signals).
  • Strategies for memory safety (async pipe, destroy hooks, takeUntilDestroyed).
  • Hybrid approach awareness: signals + RxJS bridging.
  • Mention of Angular tools: toSignal, Harness with signals, NgRx selectors.
    Weak answers: “always use RxJS” or “signals replace everything.” Strong answers: nuanced tradeoffs, hybrid patterns, and CI validation.

Preparation Tips

Build a demo app with a counter (signal) and a user list from API (RxJS). Mark components OnPush. Test stale views: mutate vs reassign arrays. Wrap Observables with async pipe; try manual subscribe and show leaks. Add distinctUntilChanged to selector to reduce emissions. Convert Observable → signal using toSignal for hybrid flow. Profile CD cycles in Angular DevTools before/after OnPush + signals. Practice explaining: signals = fine-grained local state; RxJS = async/global orchestration. Prepare a 60–90s pitch covering immutability, stale view prevention, emissions reduction, and memory safety.

Real-world Context

A SaaS dashboard replaced default CD with OnPush and signals for widgets, reducing CD cost by 40%. Local filters were signal-driven, while global data came from NgRx selectors. Another fintech app had memory leaks from manual subscriptions; switching to async pipe and takeUntilDestroyed eliminated them. An e-commerce platform used toSignal to bridge RxJS order streams into UI signals, improving performance and developer ergonomics. Each case showed hybrid design: RxJS for async flows, signals for rendering, OnPush for performance, plus immutability and cleanup to prevent stale views or leaks.

Key Takeaways

  • Signals + OnPush: local, granular, performant.
  • RxJS facades/selectors: global, async orchestration.
  • Avoid stale views with immutability and markForCheck.
  • Cut emissions with distinctUntilChanged or computed signals.
  • Prevent leaks with async pipe, destroy hooks, effect cleanup.

Practice Exercise

Scenario: You’re building an Angular analytics dashboard. It has global user data from API + NgRx, and local widgets with toggles and filters. On mid-tier devices, performance lags and memory keeps growing.

Tasks:

  1. Refactor widgets to use signals with OnPush. Replace direct mutations with reassignments.
  2. Keep global user data in RxJS selectors. Add distinctUntilChanged to reduce emissions.
  3. Bridge a stream (active sessions) into a signal with toSignal.
  4. Validate change detection cycles before/after with Angular DevTools.
  5. Test stale view prevention by toggling filters rapidly.
  6. Simulate memory leaks with manual subscribe; then fix with async pipe + takeUntilDestroyed.
  7. Profile heap snapshots to confirm stable memory.

Exercise: Prepare a 90s pitch explaining when to use signals vs RxJS, how OnPush improves performance, and what techniques you used to prevent stale views, unnecessary emissions, and memory leaks.

Still got questions?

Privacy Preferences

Essential cookies
Required
Marketing cookies
Personalization cookies
Analytics cookies
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.