The new way of subscribing in an Angular Component

Picture of a tam.
Photo by Gabor Koszegi on Unsplash

Observables and RxJS are considered the cornerstones of the Angular framework. The RxJS library allows us to create our applications in a reactive way. Nevertheless, observables come with one pretty large downside. Every time you subscribe to an observable you have to remember that you must unsubscribe from it later. Otherwise, you will create a potential bug in your application or worse, a memory leak. Dealing with it and remembering about it every time you subscribe to an observable can be a nuisance.

Over the years developers have created countless methods of managing subscriptions. Still, the community hasn’t agreed on one unified solution. I’m a big fan of choosing one practice and sticking to it, so the application looks like it has been written by one developer and the code is consistent throughout the project.

Angular version 14 recently came out and it introduced a couple of new features. One of them allowed creating a new way of subscription management. In this article, I would like to show you this method called ReactiveContext.

The complete implementation can be found in the Github repository Functional Angular.

Subscription management

Let’s take a look at the example of observable usage and the most common subscription management technique in a component:

Example component using observable
Example component using observable

As you can see in the component we are subscribing to a stream of click events. In order to do it properly, we need to unsubscribe from the stream when the component is destroyed. Otherwise, we will create a memory leak or a potential bug. The most common approach is the “takeUntil” method which requires to:

  • create a Subject and assign it to the property unsubscribe$,
  • implement the ngOnDestroy method that next value and completes the unsubscribe$ Subject,
  • add takeUntil operator before the subscribe is invoked.

If you think about it that is kind of a lot of work. That is why we have already so many libraries for subscription management and why trying to find the best solution is so important.

ReactiveContext

Now let’s take a look at what the previous example looks like when using the ReactiveContext method:

Example component using reactive context for subscription management
Example component using reactive context for subscription management

All you have to do is to wrap your observable in a useReactiveContext function. It will automatically unsubscribe from an observable when the component is destroyed.

In comparison to the previous “takeUntil” method, you don’t have to:

  • remember about creating a dedicated Subject and implement ngOnDestroy method,
  • use the takeUntil operator.

Implementation

The useReactiveContext function wraps your observable in a context and allows you to forget about subscription management. You can easily just subscribe to your stream and don’t think about what will happen when a component is destroyed. Let’s take a look at the basic implementation to see how it works under the hood:

export function useReactiveContext<T>(stream$: Observable<T>) {

  const changeDetector = inject(ChangeDetectorRef),
    unsubscribe$ = new Subject<void>();
	
  (changeDetector as ViewRef).onDestroy(() => {
    unsubscribe$.next();
    unsubscribe$.complete();
  });
	
  return {
    subscribe(
      next?: (value: T) => void,
      error?: (e: any) => void,
      complete?: () => void
    ): Subscription {
      return stream$
        .pipe(takeUntil(unsubscribe$))
        .subscribe(next, error, complete);
    },
  };
}

Basically, the useReactiveContext function takes an observable as an argument and wraps it with the takeUntil operator. Since Angular 14 ChangeDetectorRef allows accessing a new method onDestroy that lets you to run your code when a component is destroyed. In the method above we have leveraged that fact and used it to automatically clean the subscription. The useReactiveContext function creates a reference to the ChangeDetectorRef using the new inject function. This way you don’t have to pass a reference to the ChangeDetectorRef as a function argument.

Unfortunately, the inject function comes with some downsides. It can be used only in a constructor or on class properties. That means the useReactiveContext function can also only be used in these exact places. I will show you a workaround in the next part of this article.

Outside of constructor

The inject mechanism narrows the places where the useReactiveContext function can be used. Fortunately, there are two workarounds that omit this issue.

Assign ReactiveContext to a class property

You can create a class property that holds a reference to the context created by the useReactiveContext function. That solution allows you to use reactive context in class methods outside of the constructor scope:

The useReactiveContext assigned to a class property
The useReactiveContext assigned to a class property

In order for this to work you need to add a connect method that connects observable with a reactive context:

export function useReactiveContext<T>(stream$: Observable<T>) {

  const changeDetector = inject(ChangeDetectorRef),
    unsubscribe$ = new Subject<void>(),
    innerStream$ = stream$;

  let innerStream$: Observable<T> | undefined;

  if (stream$) {
    innerStream$ = stream$.pipe(takeUntil(unsub$));
  }

  (changeDetector as ViewRef).onDestroy(() => {
    unsubscribe$.next();
    unsubscribe$.complete();
  });

  return {
    connect: (stream$: Observable<T>) => {
      innerStream$ = stream$.pipe(takeUntil(unsub$));
      return context;
    },
    subscribe(
      next?: (value: T) => void,
      error?: (e: any) => void,
      complete?: () => void
    ): Subscription {
      return innerStream$
        .pipe(takeUntil(unsubscribe$))
        .subscribe(next, error, complete);
    },
  };
}

Manually pass a reference to the ChangeDetectorRef

Another solution is to pass a ChangeDetectorRef as an argument of the useReactiveContext:

The useReactiveContext with manually passed ChangeDetectorRef
The useReactiveContext with manually passed ChangeDetectorRef

This way we don’t have to rely on the inject function, so there are no downsides to reducing the scope of places where it can be used:

export function useReactiveContext<T>(
  stream$: Observable<T>,
  cd?: ChangeDetectorRef
) {

  const changeDetector = cd ? cd : inject(ChangeDetectorRef),
    unsubscribe$ = new Subject<void>(),
    innerStream$ = stream$;

  let innerStream$: Observable<T> | undefined;

  if (stream$) {
    innerStream$ = stream$.pipe(takeUntil(unsubscribe$));
  }

  (changeDetector as ViewRef).onDestroy(() => {
    unsubscribe$.next();
    unsubscribe$.complete();
  });

  const context = {
    connect: (stream$: Observable<T>) => {
      innerStream$ = stream$.pipe(takeUntil(unsubscribe$));
      return context;
    },
    subscribe(
      next?: (value: T) => void,
      error?: (e: any) => void,
      complete?: () => void
    ): Subscription {
      return innerStream$
        .pipe(takeUntil(unsubscribe$))
        .subscribe(next, error, complete);
    },
  };
  return context;
}

Observable in the template

So far we have discussed examples that strictly focus on using observables with browser events. Observables are commonly used with a template of a component. The most popular solution for that is to use:

  • async pipe — which I'm not a fan of, causes of bad performance,
  • rx-angular — rxLet, rxFor, rxIf directives solve async pipe issues and are the recommended way of subscribing to an observable in a component template.

Apart from the above-mentioned solutions, there are some other cases where you have to subscribe to an observable in a component and run detect changes manually:

Example component with detectChanges used in the subscribe
Example component with detectChanges used in the subscribe

This code can be simplified by using the useReactiveContext function:

The useReactiveContext used with detectChanges
The useReactiveContext used with detectChanges

The useReactiveContext already uses ChangeDetectorRef so we can take advantage of that and use it to manually trigger detectChanges when value appears in an observable. Instead of using subscribe use subscribeAndRender:

export function useReactiveContext<T>(stream$: Observable<T>) {

  const changeDetector = inject(ChangeDetectorRef),
    unsubscribe$ = new Subject<void>();

    // ...

  return {
    // ...
    subscribeAndRender(
      next?: (value: T) => void,
      error?: (e: any) => void,
      complete?: () => void
    ): Subscription {
      return innerStream$.subscribe(
        (v) => {
          next(v);
          changeDetector.detectChanges();
        },
        error,
        complete
      );
    },
  };
}

This solution works perfectly in an application that uses ChangeDetectionStrategy.onPush or has ngZones disabled.

Conclusions

The reactive context is a new way of dealing with subscriptions in a component. It uses mechanics that were implemented in the latest release of Angular version 14. The useReactiveContext function has a clean and flexible API. As with every solution, it comes with some pros and cons.

Pros

  • automatic unsubscribe, don't have to remember about the takeUntil operator,
  • it uses the functional approach - it's easy to use and read,
  • no boilerplate - in comparison to other solutions,
  • it offers great performance,
  • works with the onPush strategy, as well it can be used in a zone less applications.

Cons

  • using reactiveContext outside of constructor or class properties requires passing reference to the ChangeDetectorRef,
  • can only be used in a Component, Directive or Pipe,
  • unfortunately, this solution doesn't work when a component uses a @ViewChild mechanism(hope that the Angular team will fix this in the future).

Disclaimer

The solution presented in this article is an experimental approach as it uses the onDestroy method from the ViewRef . It is used to unsubscribe from all observables when an instance of a component is destroyed. The onDestroy method is not available on the ChangeDetectorRef instead, type projection has to be used:

Experimental onDestroy method
Experimental onDestroy method

Functional Angular Repository

The complete implementation can be found in the Github repository Functional Angular.

Final thoughts

When it comes to subscription management Angular community cannot agree on one final method. The latest versions of Angular introduce new features that inspire us to find better solutions for common issues. I really suggest checking out this new possibility of subscribing to an observable in a component.

Subscription management is a nuisance when it comes to using RxJS with Angular. It is hard to imagine how much time developers spend on making sure their applications are memory leak free. I hope in the future we will have one commonly used mechanism and won't have to bother ourselves with RxJS subscriptions.

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.