How to forget about the management of subscriptions and change detection in Angular

Picture of a drink.
Photo by 五玄土 ORIENTO on Unsplash

I find that most of the articles regarding the Angular framework describe issues with subscription management and change detection mechanisms. The are plenty of texts describing which unsubscribing technique is the best. Same thing about articles covering the mechanism of change detection. I personally find it tiring to always check whether the stream deals with the subscription or component manually triggers a change detection cycle. In this article, I will present an alternative of how to deal with this issue.

Common case

In my practice I’ve seen lots of projects, most of them use the async pipe to deal with reactive data, but in some cases, it cannot be used. In these cases components usually look like that:

@Component({
   changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserComponent implements OnInit, OnDestroy {

   users: ReadonlyArray<User> = [];

   private readonly destroy$ = new Subject();

   constructor(private readonly cd: ChangeDetectorRef,
            private readonly usersService: UsersService) {
   }

   ngOnInit() {
      this.usersService
          .selectAll()
          .pipe(
             takeUntil(this.destroy$)
          )
          .subscribe((users: ReadonlyArray<User>) => {
             this.users = users;
             this.cd.detectChanges();
          });
   }

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

}

This UserComponent presents data that comes from a reactive stream of users. In order to keep high performance, it uses the OnPush strategy. By taking a glimpse at the subscribe function we can see that after a new value comes from the observable, component triggers detect changes, so the template will be rebuilt.

It’s really tricky to remember to unsubscribe and to trigger detect changes. It is the main cause of many bugs that can appear in the application. I find that these two topics are also the focal point in many code reviews. Developers during the code review waste time trying to find missing unsubscribe. The other thing I guess we are all aware of is that missing unsubscribe is the number one cause of memory leaks in most angular applications. There must be a solution that helps us avoid these issues.

Memory leaks are a common issue in Angular applications.
Memory leaks are a common issue in Angular applications.

BaseComponent

Let’s take a look at the different approach:

abstract class BaseComponent implements OnDestroy {

   private readonly destroy$ = new Subject();

   protected constructor(
      protected readonly detector: ChangeDetectorRef) {
   }

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

   subscribeAndRender<T>(
      stream$: Observable<T>,
      callback: (args: T) => void
   ): void {

      stream$
         .pipe(
            takeUntil(this.destroy$)
         )
         .subscribe((subscribeArguments: any) => {
            callback(subscribeArguments);
            this.detector.detectChanges();
         });
   }

}

The BaseCompoment class has the subscribeAndRender method that allows you to forget about subscriptions and change detection. Every time you want to observe reactive data that will be used in the template you simply do it with the subscribeAndRender method. It takes a stream as an argument then uses takeUntil technique to manage observable subscriptions and subscribes to it. The operator takeUntil is used as the last operator before subscribing so it fixes lots of issues with not-closed observables, you can read more about it in this article.

It is really easy to forget about the observable subscription.
It is really easy to forget about the observable subscription.

The callback method is invoked inside subscribe method followed by the detectChanges. Now you can be sure that in high-performance components template is always re-rendered after a new value appears.

Let’s have a look at the UserComponent after changes:

class UserComponent extends BaseComponent implements OnInit {

   constructor(detector: ChangeDetectorRef) {
      super(detector);
   }

   ngOnInit() {
      this.subscribeAndRender(
         this.usersService.selectAll(),
         (users: ReadonlyArray<User>) => {
            this.users = users;
         }
      );
   }

}

As you can see it is much cleaner and it’s way easier to understand. A programmer who is reading it can focus on what component actually does and not waste time on less important things. Subscription handling doesn't shadow the code, this way we are avoiding the possibility of making it harder to read.

This approach allows not thinking about memory leaks.
This approach allows not thinking about memory leaks.

Summary

This technique uses inheritance so when you want to use the method from the BaseComponent in your application you have to extend it. It is an additional effort but this way you can be sure that your application is free from memory leaks and not updated templates. This approach will save you lots of unwanted debugging and make your application way more stable.

Luke, Software engineer, passionate about best practices, frameworks React, Vue, patterns and architectures for modern web
				development, open-source creator, TypeScript enthusiast, Generic UI Technical Blog

Luke

Software engineer, passionate about best practices, frameworks React & Vue, patterns and architectures for modern web development, open-source creator, TypeScript enthusiast.

We use cookies to improve your experience. If you continue browsing, we assume that you consent to our use of cookies.