DEV Community

Diego Liascovich
Diego Liascovich

Posted on

πŸš€ How I Migrated an Angular App from RxJS Observables to Signals: Real Case Study

By Diego Liascovich

Full-Stack Developer | Microservices | Angular | Node.js

In this post, I share how I migrated a real-world Angular application from heavy use of Observables and async pipes to the new reactive model based on signal() introduced in Angular 16+.

I walk through real decisions, code examples (before/after), and practical lessons learned.


🧠 Why Migrate from Observables to Signals?

RxJS is powerful, but can get overly complex with:

  • switchMap, combineLatest, Subject, unsubscribe, etc.
  • Harder debugging and lifecycle issues.
  • Performance pitfalls if subscriptions aren't handled properly.

With signals:

  • No need to manually subscribe or unsubscribe.
  • More predictable rendering.
  • Cleaner code and localized reactive state.

πŸ—οΈ Real App Context

A real Angular 15 app used for managing orders and invoices (orders, billings), using RxJS, was migrated to Angular 17.

It used BehaviorSubject, Observable, combineLatest, and async pipe extensively for data and UI state.


πŸ” Before: RxJS-Based Code

// orders.service.ts
private ordersSubject = new BehaviorSubject<Order[]>([]);
orders$ = this.ordersSubject.asObservable();

loadOrders() {
  this.http.get<Order[]>('/api/orders')
    .subscribe(data => this.ordersSubject.next(data));
}
Enter fullscreen mode Exit fullscreen mode
<!-- orders.component.html -->
<div *ngIf="orders$ | async as orders">
  <app-order *ngFor="let order of orders" [data]="order"></app-order>
</div>
Enter fullscreen mode Exit fullscreen mode

βœ… After: Signal-Based Code

// orders.service.ts
import { signal } from '@angular/core';

orders = signal<Order[]>([]);

loadOrders() {
  this.http.get<Order[]>('/api/orders')
    .subscribe(data => this.orders.set(data));
}
Enter fullscreen mode Exit fullscreen mode
// orders.component.ts
readonly orders = this.ordersService.orders;
Enter fullscreen mode Exit fullscreen mode
<!-- orders.component.html -->
<div *ngFor="let order of orders()">
  <app-order [data]="order"></app-order>
</div>
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ Migrating Derived Logic (e.g., combineLatest)

Before:

// Combining filter and data
readonly filteredOrders$ = combineLatest([
  this.orders$,
  this.filter$
]).pipe(
  map(([orders, filter]) => orders.filter(order => order.status === filter))
);
Enter fullscreen mode Exit fullscreen mode

After (with computed and signal):

readonly filter = signal('pending');

readonly filteredOrders = computed(() =>
  this.orders().filter(order => order.status === this.filter())
);
Enter fullscreen mode Exit fullscreen mode

πŸ”§ Important Considerations

🧹 No More Manual Subscriptions

With signal(), Angular handles lifecycle cleanup. No need for takeUntil, unsubscribe, or memory leak worries.

πŸ”„ Mixing RxJS and Signals

To convert an Observable to a Signal:

readonly user$ = this.authService.user$;
readonly user = toSignal(this.user$, { initialValue: null });
Enter fullscreen mode Exit fullscreen mode

To go the other way:

const user$ = fromSignal(this.user);
Enter fullscreen mode Exit fullscreen mode

⚠️ When Not to Migrate (Yet)

  • Your app heavily depends on NgRx or RxJS-centric libs.
  • You have mature RxJS-based logic with full test coverage.
  • You're using Angular < 16.

🧭 Conclusion

Migrating to Signals allowed me to:

  • Simplify component code.
  • Eliminate manual subscriptions.
  • Improve app performance and readability.

You don’t need to migrate all at once. You can combine RxJS and Signals in the same app and migrate incrementally.


πŸ™Œ Enjoyed this?

If you're thinking of migrating your Angular app to Signals, I hope this post gives you some real insight and confidence.

Drop a comment if you have questions or want to share your experience!

Top comments (0)