Using dependency injection to automatically unsubscribe from an Observable
Tags: webdev, angular
Published: 2022-12-08 04:14:08

## The duplicated pattern

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

```typescript
@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();
  }
}
```

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

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

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

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.

```typescript
@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();
  }
}
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kzacxke4wgxwnvzqdo0v.png)


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](https://blog.angular.io/ivys-internal-data-structures-f410509c7480).

## 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.

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

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](https://www.npmjs.com/package/eslint-plugin-ngx-eslint)

## Using `DestroyService` with `inject` function from Angular 14

Angular 14 introduces the [inject](https://angular.io/api/core/inject#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`

```typescript
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();

```

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

```typescript
@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();
  }
}
```

## 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

- https://twitter.com/yjaaidi/status/1540068147084001284?s=61&t=j2DvQJGwVaHHrkYEWLv2JQ
- https://twitter.com/waterplea/status/1271037735332204544?s=61&t=j2DvQJGwVaHHrkYEWLv2JQ