RxJS best practices in Angular

Posted on September 15, 2023
angularBest Practicesrxjs

Angular, one of the leading front-end frameworks, brings the mighty RxJS library to the forefront for handling asynchronous operations. While RxJS empowers developers to master intricate data flows seamlessly, it also introduces a set of challenges when not wielded with care. Memory leaks, the labyrinth of nested subscribes, and improper use of observables can all cast a shadow on your Angular application's performance and maintainability.

In this blog, we embark on a journey through essential RxJS best practices. We'll traverse the landscape of code pitfalls and pristine examples to equip you with the knowledge to not only harness the full potential of RxJS but also craft Angular applications that are elegant, efficient, and devoid of any lurking issues. So, let's dive headfirst into the realm of RxJS, unravel the mysteries, and emerge with the expertise to create Angular applications that are as robust as they are delightful to develop.

Avoiding Memory Leaks: Memory leaks can be a significant issue in Angular applications when using RxJS. They occur when you don't properly unsubscribe from observables, causing the application to retain references to objects that should be disposed of, leading to increased memory consumption over time.

Bad Example: In this example, we create an observable that emits data at regular intervals and subscribe to it. However, we forget to unsubscribe from the observable when the component is destroyed, leading to a memory leak.

import { Component, OnInit } from '@angular/core';
import { Observable, Subscription } from 'rxjs';

@Component({
  selector: 'app-memory-leak',
  template: '<div>{{ data }}</div>',
})
export class MemoryLeakComponent implements OnInit {
  data$: Observable<number>;
  subscription: Subscription;

  ngOnInit() {
    this.data$ = new Observable(observer => {
      setInterval(() => {
        observer.next(Math.random());
      }, 1000);
    });

    this.subscription = this.data$.subscribe(data => {
      this.data = data;
    });
  }
}

Good Example: In the improved code, we use the takeUntil operator to automatically unsubscribe from the observable when the component is destroyed, preventing memory leaks.

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Observable, interval, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

@Component({
  selector: 'app-memory-leak',
  template: '<div>{{ data }}</div>',
})
export class MemoryLeakComponent implements OnInit, OnDestroy {
  data$: Observable<number>;
  private destroy$: Subject<void> = new Subject<void>();

  ngOnInit() {
    this.data$ = interval(1000);

    this.data$
      .pipe(takeUntil(this.destroy$))
      .subscribe(data => {
        this.data = data;
      });
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

Avoiding Nested Subscribes: Nested subscribes can lead to callback hell and make your code harder to read and maintain. It's a common anti-pattern in RxJS. To avoid this, you can use operators like switchMap, mergeMap, or concatMap to flatten and manage the nested observables.

Bad Example: In this example, we nest two subscriptions, making the code less readable and harder to manage.

this.userService.getUser().subscribe(user => {
  this.authService.isLoggedIn().subscribe(isLoggedIn => {
    if (isLoggedIn) {
      this.displayUser(user);
    } else {
      this.redirectToLogin();
    }
  });
});

Good Example: In the improved code, we use the switchMap operator to flatten the nested observables and make the code more readable and maintainable.

this.userService.getUser().pipe(
  switchMap(user => this.authService.isLoggedIn().pipe(
    map(isLoggedIn => ({ user, isLoggedIn }))
  ))
).subscribe(({ user, isLoggedIn }) => {
  if (isLoggedIn) {
    this.displayUser(user);
  } else {
    this.redirectToLogin();
  }
});

Avoiding Manual Subscribes in Angular: In Angular, you often work with observables returned by services. Instead of manually subscribing to these observables in your components, you can assign them directly to class properties and use the async pipe in the template to manage subscriptions automatically.

Bad Example: In this example, we manually subscribe to an observable returned by a service.

this.userService.getUser().subscribe(user => {
  this.user = user;
});

Good Example: In the improved code, we assign the observable directly to a class property and use the async pipe in the template to manage subscriptions automatically.

ngOnInit() {
  this.user$ = this.userService.getUser();
}

Don't Pass Streams to Components Directly: Passing observables directly from parent to child components can lead to unexpected behavior and makes your components tightly coupled. It's a good practice to let child components fetch their own data when needed.

Bad Example: In this example, we pass an observable from a parent component to a child component.

// In parent component
<app-child [data]="data$"></app-child>

// In child component
@Input() data$: Observable<any>;

Good Example: In the improved code, the child component fetches its data independently, reducing the coupling between parent and child components.

// In parent component
<app-child></app-child>

// In child component
ngOnInit() {
  this.data$ = this.dataService.getData();
}

Don't Pass Streams to Services: Services in Angular should encapsulate data retrieval and manipulation logic. They should not expect observables to be passed to them from components. Instead, services should directly return observables or other values.

Bad Example: In this example, a service expects an observable to be passed to it.

@Injectable()
export class DataService {
  constructor(private http: HttpClient) {}

  fetchData(data$: Observable<any>) {
    return data$.pipe(
      // ...
    );
  }
}

Good Example: In the improved code, the service directly returns an observable.

@Injectable()
export class DataService {
  constructor(private http: HttpClient) {}

  fetchData() {
    return this.http.get<any>('...');
  }
}

Sharing Subscriptions: Sharing subscriptions is important to prevent multiple HTTP requests or unnecessary side effects when multiple subscribers are involved. You can use operators like shareReplay to share a single subscription among multiple observers.

Bad Example: In this example, multiple subscribers cause multiple HTTP requests.

const data$ = this.http.get<any>('...');
data$.subscribe(result => {
  // Handle result
});

data$.subscribe(result => {
  // Handle result again, causing two requests
});

Good Example: In the improved code, the shareReplay operator is used to share the result of the HTTP request among multiple subscribers.

const data$ = this.http.get<any>('...').pipe(shareReplay());

data$.subscribe(result => {
  // Handle result
});

data$.subscribe(result => {
  // Reuses the same subscription
});

When to Use Subjects: Subjects are a powerful but potentially risky feature in RxJS. They can be useful for scenarios like event broadcasting, but their use should be limited to cases where they are truly necessary.

Bad Example: In this example, a subject is used without a clear reason, which can lead to unexpected behavior.

@Injectable()
export class DataService {
  private dataSubject = new Subject<any>();
  data$ = this.dataSubject.asObservable();

  updateData(data: any) {
    this.dataSubject.next(data);
  }
}

Good Example: In the improved code, a BehaviorSubject is used when maintaining and sharing the last emitted value is necessary, providing better control and predictability.

@Injectable()
export class DataService {
  private dataSubject = new BehaviorSubject<any>(null);
  data$ = this.dataSubject.asObservable();

  updateData(data: any) {
    this.dataSubject.next(data);
  }
}

Clean Code Practices: Clean code practices are essential when working with RxJS in Angular. It includes using meaningful variable and function names, breaking down complex observables into smaller parts, properly documenting code, and ensuring type safety with TypeScript.

These best practices collectively contribute to writing more maintainable, readable, and bug-free Angular applications that use RxJS effectively. Incorporating these practices will not only enhance the quality of your code but also make it easier for you and your team to work on and maintain the application over time.

  • ✅ Use meaningful variable and function names.
  • ✅ Break complex observables into smaller, composable parts using operators.
  • ✅ Keep your subscriptions in the component and clean them up properly in ngOnDestroy using takeUntil or other mechanisms.
  • ✅ Use type safety with TypeScript and consider using interfaces for your observables.
  • ✅ Document your observable streams using comments or other documentation practices.

Thanks for reading!


Posted on September 15, 2023
Profile Picture

Arun Yadav

Software Architect | Full Stack Web Developer | Cloud/Containers

Subscribe
to our Newsletter

Signup for our weekly newsletter to get the latest news, articles and update in your inbox.