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();
}
}
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();
}
}
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();
}
}
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
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) {}
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();
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();
}
}
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.
Top comments (1)
You can also just use
TuiDestroyService
from@taiga-ui/cdk
library which has a lot more useful pipes, decorators and directives.