When I first delved into Angular, a colleague suggested using take(1)
for API calls with observables in RxJs, claiming it automatically unsubscribes after one call.
Believing this, I continued to use it until noticing significant rendering delays when navigating between pages.
Let's examine an example:
Consider a UserRepository
connecting with the API to assign application permissions:
@Injectable({
providedIn: 'root'
})
export class UserRepository {
private apiUrl: string = environment.apiUrl;
constructor(private readonly httpRequestService: HttpRequestService) {
super();
}
@UnsubscribeOnDestroy()
getPermissions() {
return this.httpRequestService.get(`${this.apiUrl}/user/permissions`).pipe(map((res: any) => res.user));
}
}
Usage:
userRepository.getPermissions().pipe(take(1)).subscribe(data => ...);
After researching, I discovered that take(1)
only takes one observable from the pipe without automatic unsubscription.
To resolve this, I considered the following solutions:
- Option #1: Implement
takeUntil
with aunsubscriptionSubject
, triggering unsubscribe onngOnDestroy
- Option #2: Create a
BaseRepository
with a dictionary ofmethodName => Subject
to manage subscriptions efficiently through a Decorator.
Example showcasing the Decorator and usage for Option #2:
BaseRepository
export abstract class BaseRepository {
protected unsubscriptionSubjects: {[key: string]: Subject<void>} = {};
}
Implementation:
@Injectable({
providedIn: 'root'
})
export class UserRepository extends BaseRepository {
private apiUrl: string = environment.apiUrl;
constructor(private readonly httpRequestService: HttpRequestService) {
super();
}
@UnsubscribeOnDestroy()
getPermissions() {
return this.httpRequestService.get(`${this.apiUrl}/user/permissions`).pipe(map((res: any) => res.user));
}
}
The decorator:
export function UnsubscribeOnDestroy() {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const instance = this as any;
const methodName = propertyKey;
instance.unsubscriptionSubjects = instance.unsubscriptionSubjects || {};
if (instance.unsubscriptionSubjects[methodName]) {
instance.unsubscriptionSubjects[methodName].next();
} else {
instance.unsubscriptionSubjects[methodName] = new Subject();
}
const result = originalMethod.apply(this, args);
return result.pipe(takeUntil(instance.unsubscriptionSubjects[methodName]));
};
return descriptor;
};
}
I opted for option #2, resolving the issues with smooth rendering and eliminating delays.
This choice was driven by the necessity to load substantial data on the same page continuously. It became evident that the longer the usage without proper unsubscription, the more sluggish the loading states would become. Maintaining clear subscriptions seemed essential for optimal functionality.
Queries I have:
- Why doesn't RxJS offer a built-in feature like axios'
then
andcatch
for single request handling? - Does my solution address the issue effectively or could it introduce unforeseen problems?
- What drives most Angular developers to utilize observables over then/catch with axios for HTTP requests?