Managing Angular subscriptions to prevent memory leaks when making API requests

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:

  1. Option #1: Implement takeUntil with a unsubscriptionSubject, triggering unsubscribe on ngOnDestroy
  2. Option #2: Create a BaseRepository with a dictionary of methodName => 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:

  1. Why doesn't RxJS offer a built-in feature like axios' then and catch for single request handling?
  2. Does my solution address the issue effectively or could it introduce unforeseen problems?
  3. What drives most Angular developers to utilize observables over then/catch with axios for HTTP requests?

Answer №1

To ensure that a subscription is properly unsubscribed, the best method is to utilize the newly introduced destroyRef along with takeUntilDestroyed


   import { DestroyRef } from '@angular/core';
   import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

    export class MyComponent implements OnInit {

    private destroy = inject(DestroyRef);

    constructor() {
        this.user$
            .pipe(takeUntilDestroyed(this.destroy))
            .subscribe((aUser: User | null) => {
                this.currentUser = aUser;
            });
    }

Answer №2

  1. RXJS serves as a valuable tool for implementing reactive programming concepts in JavaScript. By working with streams of data or events, developers using Angular may encounter challenges when mixing non-reactive code with modules such as httpClient that adhere to the reactive programming paradigm. This raises the question: if RXJS didn't exist, why not just utilize fetch directly?

  2. Your approach may not align well with reactive programming principles, as it lacks consideration for multiple subscriptions to the same observable. It's important to recognize that consumers of functions like getPermissions() may require ongoing observables rather than one-time subscriptions.

  3. The significance of adhering to the reactive programming paradigm becomes clear for Angular developers. Embracing reactive principles enhances the performance and efficiency of Angular applications, making modules like httpClient ideal for handling asynchronous operations.

Answer №3

What is the reason behind rxjs not having a built-in mechanism to take only one request and automatically unsubscribe, similar to using then and catch in axios?

Some observables handle their own finalization, while others require external finalization. For instance, HttpClient's get method completes immediately upon receiving a response.

Will my proposed solution truly solve the issue or could it potentially introduce other unforeseen problems?

While decorators can be useful, they need to be meticulously crafted. Your implementation involves utilizing a dictionary that must ensure non-overwriting of values. Additionally, if the decorated method is modified to return a non-Observable type, an error will be triggered by the decorator.

Considering the self-completion nature of HttpClient.get(...) mentioned earlier, applying rxjs operators like take(1) for completion is unnecessary in this scenario. However, there is another aspect to consider in your example. By wrapping http within a repository class, you abstract the use of http, which means there's no guarantee that the underlying implementation may not change to something generating a perpetual stream observable - necessitating the use of an rxjs operator!

Luckily, we are focused on design discussions here. A repository object, especially for query functions, should ideally return a single-emission, self-completing observable. We don't anticipate an ongoing stream from a storage query. Thus, leaving getPermissions without an rxjs operator like take(1) seems appropriate from a design perspective in my opinion.

Why do most Angular frontend developers opt for observables over then/catch with axios?

Observables align with the principles of reactive programming, unlike promise-based programming. Furthermore, observables embody streaming concepts, which many developers find easier to manage than promises.

Answer №4

Regarding your second question, your solution is commendable and necessary. It is common knowledge that the HTTP request made using HttpClient is automatically completed (as it is emitted only once).

Most developers believe there is no need to unsubscribe or use take(1) in such cases.

However, in scenarios where the HTTP request takes a considerable amount of time to complete, your approach proves to be valuable. It allows us to unsubscribe from the HTTP request before its completion.

Refer to the User component example for more details

Visit /users and then navigate away after 5 seconds. Even after leaving the User component for 10 seconds, you will notice that the messages Http 2 Response &

Execute logic when HTTP 2 response 
are still displayed in the Console.

Based on my experience, I recommend unsubscribing always to prevent unexpected side effects

ngOnInit(): void {
    this.http.get('https://jsonplaceholder.typicode.com/users').subscribe({
      next: (res) => {
        console.log('Http 1 Response ', res);
      },
      complete: () => {
        console.log('Http 1 complete ');
      },
    });

    this.http
      .get('https://jsonplaceholder.typicode.com/users')
      .pipe(delay(10000))
      .subscribe({
        next: (res) => {
          console.log('Http 2 Response ', res);
          console.log('Execute logic when http 2  response ');
        },
        complete: () => {
          console.log('Http 2 complete ');
        },
      });

    this.http
      .get('https://jsonplaceholder.typicode.com/users')
      .pipe(delay(10000), takeUntil(this.destroyed$))
      .subscribe({
        next: (res) => {
          console.log('Http 3 Response ', res);
          console.log('Execute logic when HTTP 3  response ');
        },
        complete: () => {
          console.log('Http 3 complete ');
        },
      });
  }

Answer №5

If you wrap your HTTP request with firstValueFrom, it will be converted into a Promise. This means you won't have to use subscribe() in this scenario.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Executing a service request in Angular 2 using a versatile function

I have a function that determines which service to call and a function template for calling the service returned by that function. This function makes HTTP requests using http.get/http.post, which return an Observable and then perform a map operation on th ...

Refresh Angular meta tags/data in a component with a double-click event

Currently, I have a situation where the product listing and product detail view are displayed on the same component. Initially, the product listing is shown on page load and upon clicking a specific product, the product listing is hidden while the product ...

Can you provide guidance on integrating TypeScript with React version 15.5?

I'm looking for the best approach to integrate TypeScript and React following the separation of PropTypes into a separate project with version 15.5. After upgrading from 15.4 to 15.5, everything seems to be running smoothly except for a warning in th ...

The reactivity of arrays in Vue components' props is limited

I've got an array with component data that I'm attempting to render using v-for <div :style="style" class="editor-component" v-for="(component, index) in components"> <Component :is="component.name" v-bind="component.o ...

Use Angular to trigger a method when the Enter key is pressed, passing the current input text as a parameter

I need to extract the text from an input field and pass it to a method. Here is the code: index.component.html <input type="text" (keyup.enter)="sendData()" /> index.component.ts sendData() { console.log(The text from the input field); } Can ...

A practical guide to effectively mocking named exports in Jest using Typescript

Attempting to Jest mock the UserService. Here is a snippet of the service: // UserService.ts export const create = async body => { ... save data to database ... } export const getById = async id => { ... retrieve user from database ... } The ...

Improving the process of class initialization in Angular 4 using TypeScript

Is there a more efficient method to initialize an inner class within an outer class in Angular 4? Suppose we have an outer class named ProductsModel that includes ProductsListModel. We need to send the ProductId string array as part of a server-side reque ...

`Bootstrap 4 fails to center elements vertically`

Struggling to center a panel using Bootstrap 4? Despite adding classes like align-items-center and h-100, the header continues to stay at the top of the page. Does anyone know the solution? This is in an Angular 4 project with Bootstrap. <div class=&ap ...

Before loading a deep link, use pre-CanActivate or pre-CanLoad

In my current project, I am faced with a challenging task of transitioning from Adobe Flash & Flex applications on a ColdFusion 11 backend to Angular applications. The expectation is that users will already be logged in and have an active session before a ...

Ensure that the tooltip remains visible within the confines of the page

In my Angular application, I have successfully implemented a versatile tooltip feature that goes beyond just displaying text. The tooltip is a div element that has the ability to contain various contents: .tooltip-master { position: relative; .tooltip ...

The service being injected is not defined

Two services are involved in this scenario, with the first service being injected into the second service like so: rule.service.ts @Injectable() export class RuleService { constructor( private _resourceService: ResourceService ){} s ...

Updating an Angular component: identifying the whole component as needing refresh

Is there a simpler method to set the dirty property at the component level without using ng-model and ids? I know that these can be used to access properties like dirty and pristine as stated in the Angular documentation. I have a component that contains ...

What's the issue with conducting a unit test on a component that has dependencies with further dependencies?

I am experiencing an annoying error that seems to be my mistake and I cannot figure out how to resolve it. The issue lies within a simple component which serves as a top-bar element in my web application. This component has only one dependency, the UserSe ...

Tips on utilizing the i18n t function within a TypeScript-powered Vue3 component

I am working on creating a control that can automatically manage interpolation for internationalization (i18n) strings: <script lang="ts"> import { ref, defineComponent, inject } from "vue"; import type { InterpolationItem, Inter ...

TSLint has detected an error: the name 'Observable' has been shadowed

When I run tslint, I am encountering an error that was not present before. It reads as follows: ERROR: C:/...path..to../observable-debug-operator.ts[27, 13]: Shadowed name: 'Observable' I recently implemented a debug operator to my Observable b ...

Alert: Circular dependency detected!

In an effort to have cleaner imports, I've set up a typescript file that exports all the components I'm using. For example: import { Comp1, Comp2, Comp3 } from index/components However, when using this approach, I encounter a warning during bu ...

Establish a local binding context within an Angular template

If I have a complex object structure that I need to bind to: <div>{{model.rootProperty}}</div> <div> <div>{{model.some.deeply.nested.property.with.a.donut.name}}</div> <div>{{model.some.deeply.nested.property.w ...

Tips for effectively managing 404 errors in Angular 10 with modular routing

I'm facing challenges with handling 404 pages within an Angular 10 application that utilizes modular routing architecture. Here is the structure of my code: |> app |-- app.module.ts |-- app-routing.module.ts |-- app.component{ts, spec.ts, scss, ht ...

Error message: The specified expression cannot be built using Google OAuth authentication in a Node.js environment

I'm currently working on integrating my NodeJS API, which was developed in TypeScript, with Google Oauth2 using Passport. However, when following the documentation written in JavaScript, I encountered an error underlining GoogleStrategy. This expressi ...

Simplify a function by lowering its cyclomatic complexity

This particular function is designed to determine whether a specific cell on a scrabble board qualifies as a double letter bonus spot. With a cyclomatic complexity of 23, it exceeds the recommended threshold of 20. Despite this, I am unsure of an alterna ...