DEV Community

Hien Pham for This is Angular

Posted on

Using dependency injection to automatically unsubscribe from an Observable

The duplicated pattern

In some projects that I worked on, people usually write this code to unsubscribe from an Observable in a component.

@Component({
  // component metadata
})
export class MyComponent
  implements OnInit, OnDestroy {

  private destroy$ = new Subject<void>();

  private employee$: Observable<Employee> =
    this.employeeService.getEmployeeDetails();

  ngOnInit(): void {
    this.employee$.pipe(
      takeUntil(this.destroy$),
      tap(() => {
        // do something here
      })
    ).subscribe();
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }
}
Enter fullscreen mode Exit fullscreen mode

There’s nothing wrong with the above code. But you can see the code in ngOnDestroy life cycle hook is repeated in every component. In fact, we can remove this duplication by writing an Angular service called DestroyService.

DestroyService implementation

The code for DestroyService will look like this

@Injectable()
export class DestroyService
  extends Subject<void> implements OnDestroy {

  ngOnDestroy(): void {
    this.next();
    this.complete();
  }
}
Enter fullscreen mode Exit fullscreen mode

It’s just an Observable based service which implements the OnDestroy life cycle hook, and it will emit next and complete notification on destroy.

Then we can use it in our component like this. It also works well for directives.

@Component({
  ... // omit for brevity
  providers: [DestroyService]
})
export class MyComponent implements OnInit {

  constructor(
    @Self()
    private readonly destroy$: DestroyService
  ) {}

  private employee$: Observable<Employee> =
    this.employeeService.getEmployeeDetails();

  ngOnInit(): void {
    this.employee$.pipe(
      takeUntil(this.destroy$),
      tap(() => {
        // do something here
      })
    ).subscribe();
  }
}
Enter fullscreen mode Exit fullscreen mode

Now in your component, you don’t need to implement OnDestroy anymore because the DestroyService already did that for you. You just need to inject it in constructor and provide it in the provider list.

How does DestroyService work?

The DestroyService implements OnDestroy life cycle hook, so every time you provide it in the component’s providers array, the service knows when the component is destroyed, then it emits a notification.

Whenever a component gets destroyed, the takeUntil operator will do the job to automatically unsubscribed from the observable.

In Chrome browser, you can see the destroy hook of the component by selecting this component, then navigate to console tab and type ng.getInjector($0).lView[1].destroyHooks, the result will look somehow like this

The destroyHooks of the component

From the console, we can see that the component has one destroy hook. It is the ngOnDestroy function at DestroyService.ts, line number 6.

More about Ivy internal data structure is here.

Usage note

Although the DestroyService can help eliminate the code duplication in components or directives, people sometimes forget to provide DestroyService in component’s providers, therefore the Observable wouldn’t unsubscribed properly.

There’s a way to avoid this pitfall by adding @Self() DI decorator when injecting the DestroyService. In this case, Angular will help to throw an error if we forgot adding DestroyService in component’s providers at run time.

constructor(@Self() private destroy$: DestroyService) {}
Enter fullscreen mode Exit fullscreen mode

There is another way to detect this mistake at development time. It is when custom ESLint rule comes in handy. Fortunately, I already implemented a custom rule for it and it is available at this link

Using DestroyService with inject function from Angular 14

Angular 14 introduces the inject function which allows us to inject a token from the currently active injector.

We can write the DestroyService mentioned above by using the inject function as follow.

Create a file named describe-destroy-service.ts

import { ClassProvider, inject, Injectable, OnDestroy } from '@angular/core';
import { Observable, Subject } from 'rxjs';

function describeDestroyService() {
  @Injectable()
  class DestroyService
    extends Subject<void> implements OnDestroy {

    ngOnDestroy(): void {
      this.next();
      this.complete();
    }
  }

  function provideDestroyService(): ClassProvider {
    return {
      provide: DestroyService,
      useClass: DestroyService,
    };
  }

  function injectDestroyService(): Observable<void> {
    const destroy$ = inject(DestroyService, { self: true, optional: true });

    if (!destroy$) {
      throw new Error(
        'It seems that you forgot to provide DestroyService. Try adding "provideDestroyService()" to your declarable\'s providers.'
      );
    }

    return destroy$.asObservable();
  }

  return {
    provideDestroyService,
    injectDestroyService,
  };
}

export const { provideDestroyService, injectDestroyService } =
  describeDestroyService();

Enter fullscreen mode Exit fullscreen mode

We expose the provideDestroyService and injectDestroyService functions. Here is how we can use it in component

@Component({
  ... // omit for brevity
  providers: [provideDestroyService()]
})
export class MyComponent implements OnInit {
  // The `DestroyService` is injected
  // by using `inject` function behind the scene
  // rather than in constructor
  private readonly destroy$ = injectDestroyService();

  private employee$: Observable<Employee> =
    this.employeeService.getEmployeeDetails();

  ngOnInit(): void {
    this.employee$.pipe(
      takeUntil(this.destroy$),
      tap(() => {
        // do something here
      })
    ).subscribe();
  }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this article, I introduce you another use case of dependency injection to automatically unsubscribe from an observable in components or directives in order to eliminate boilerplate code.

I also explain how the DestroyService works and how to use it efficiently by adding DI decorator @Self() as well as by using custom ESLint rule.

Thank for your time and happy coding.

References

Oldest comments (1)

Collapse
 
hakimio profile image
Tomas Rimkus • Edited

You can also just use TuiDestroyService from @taiga-ui/cdk library which has a lot more useful pipes, decorators and directives.