Angular – Auto-unsubscribe for RxJS

Ildar Timerbaev
Webtips
Published in
4 min readJul 19, 2021

Hello dear readers! Angular is a very powerful framework. A huge part of this framework and not less by power is ReactiveJS. Angular uses Rxjs almost everywhere. But Rxjs has a little, not obvious mechanism called Subscriptions.

Rxjs observable is lazy. So you have to subscribe to an observable to start data streaming. When we subscribe we should unsubscribe later. Anyway. If we will do a subscription in a component and change some state or there are operations that can change state outside of component we have a potentially risky subscription. What does it mean? With this subscription, we can’t say that the current state is valid because it can be changed from an unexpected place. Let’s reproduce it in the example below.

@Component(...)
export class MyComponent {
constructor(private apiService: MyApiService) {} public ngOnInit(): void {
this.apiService
.callSomeServerApi()
.subscribe(() => window.location = 'https://another.site`)
}
}

So if we destroy MyComponent for some reason(e.g. *ngIf directive) and we have a very slow connection to the server then after destroying the component we still will wait for the response from the HTTP request and in a successful case, we will be redirected. In sum, we destroyed the component and do not expect any action from it, but the subscription is still alive.

Built-in subscriptions manage

As the solution to these problems, we can unsubscribe before a component will destroy.

Automatically unsubscribe with async pipe

The most right solution is to do subscriptions in the template with the async pipe. Its built-in pipe will subscribe on component view initialization and will unsubscribe when a component will be destroyed.

But this solution not working if we use subscription in directives or can’t subscribe in templates.

Manually unsubscribe with Subscription

We can store subscriptions as component property and unsubscribe in the onDestroy hook. But this solution requires a lot of boilerplate code(see below).

@Component(...)
export class MyComponent implements OnDestroy {
private subscription: Subscription; // boilerplate
constructor(private apiService: MyApiService) {} public ngOnInit(): void {
this.subscription = this.apiService // boilerplate
.callSomeServerApi()
.subscribe(() => window.location = 'https://another.site`)
}
public ngOnDestroy(): void {
if (this.subscription) { // boilerplate
this.subscription.unsubscribe(); // boilerplate
} // boilerplate }}

Manually unsubscribe with takeUntil operation

Also, we can use takeUntil operation and Rxjs Subject for manage subscriptions (see below). This format also has a lot of boilerplate code.

@Component(...)
export class MyComponent implements OnDestroy {
private unsubscribe: Subject; // boilerplate
constructor(private apiService: MyApiService) {} public ngOnInit(): void {
this.subscription = this.apiService
.callSomeServerApi()
.pipe(takeUntil(this.unsubscribe)) // boilerplate
.subscribe(() => window.location = 'https://another.site`)
}
public ngOnDestroy(): void {
this.unsubscribe.next(); // boilerplate
this.unsubscribe.complete(); // boilerplate
}}

How to unsubscribe automatically

Digression: We have a lot of subscriptions in our work project and we can’t use the async pipe. Project architecture allows us to use only directives and components. So we have about 50 directives and slightly fewer components. We can’t afford to write boilerplate code with Subscription or takeUntil operation. So we wanted to write an easy-to-use mechanism that will unsubscribe automatically.

We should store our subscriptions in components and unsubscribe them when the component destroying. The best place for it is theOnDestroy hook. Also, our solution should work in AOT and JIT modes.

@AutoUnsubscribe

Since we use TypeScript and Angular Framework the best mechanism for manage subscriptions is property decorators. The full code of decorator is below.

export function AutoSubscribe(target: any, key: string, descriptor: PropertyDescriptor): void {
const originalOnDestroy = target.ngOnDestroy;
target._subscriptions = target._subscriptions || new Map<string, Subscription>();
const originalMethod = descriptor.value;

descriptor.value = function (...args: any[]) {
const subscription = originalMethod.apply(this, args).subscribe();
if (this._subscriptions.has(key)) {
this._subscriptions.get(key).unsubscribe();
}
this._subscriptions.set(key, subscription);
};
if (typeof target.ngOnDestroy !== 'function') {
throw new Error('@AutoSubscribe: ngOnDestroy hook not provided. Please implement your class from AutoUnSubscribe');
}
if (!target._subscriptionsOnDestroyRegistered) {
target.ngOnDestroy = function (...args: any[]) {
Array.from((this._subscriptions as Map<string, Subscription>).entries()).forEach(([mapKey, sub]) => {
sub.unsubscribe();
(this._subscriptions as Map<string, Subscription>).delete(mapKey);
});
originalOnDestroy.apply(this, args);
};
target._subscriptionsOnDestroyRegistered = true;
}
}
export type AutoUnsubscribe = OnDestroy;

Example of usage

@Directive()
export class MyDirective implements AutoUnsubscribe {
public ngOnDestroy(): void {}

@AutoSubscribe
public myMethod(): Observable<any> {
return of();
}
}

How it works

We created the decorator which modifies the original method: it will subscribe returned Observable and push to general subscriptions map-storage. Each storage item has a unique key(unique class method name).

Also, this decorator will extend onDestroy hook method: it will unsubscribe from all active subscriptions and call original onDestroy hook function.

But only one moment: Angular requires any hooks declaration directly as a component class method. So we must create theAutoUnsubscribe type which will require OnDestroy hook.

That’s all! Thank you for reading.

If you like any of the work and if it had any value to you, do please consider buying me a coffee. It makes me happy and allows me to keep creating. https://www.patreon.com/vicev

Our telegram channel: https://t.me/frontend_html_css_js

--

--