Utility managing various asynchronous tasks through observables and signaling mechanism

Looking for a Signal-based utility to monitor the status of multiple asynchronous operations carried out with observables (such as HTTP calls). This will enable using those signals in Components that utilize the OnPush change detection strategy.

Imagine having an AnimalService used for fetching animals:


        @Injectable({
            providedIn: 'root',
        })
        export class AnimalService {
            private readonly httpClient = inject(HttpClient);
        
                fetchCats() {
                    return this.httpClient.get('animals.org/cats');
                }
        
                fetchDogs() {
                    return this.httpClient.get('animals.org/dogs');
                }
        
                fetchChickens() {
                    return this.httpClient.get('animals.org/chickens')
                }
        }
    

What I need is a centralized system that keeps tabs on when certain animals are being loaded. Additionally, I want to be able to monitor this information from various locations that may not necessarily know where the requests are made. For instance, if Component A triggers the fetch calls, Component B should be able to determine whether animals are being loaded in order to display a spinner.

Answer №1

My resolution to this problem involved the development of a versatile LoadingStatusManager utility, which appears as follows:

import { computed, Signal, signal, WritableSignal } from '@angular/core';
import { catchError, Observable, tap } from 'rxjs';

export class LoadingStatusManager<KeyType extends string> {
  private readonly statusMap = {} as { [key in KeyType]: WritableSignal<boolean> };

  constructor(private readonly keys: KeyType[]) {
    for (const key of keys) {
      this.statusMap[key] = signal(false);
    }
  }

  executeAndUpdateStatus<T>(key: KeyType, observable: Observable<T>): Observable<T> {
    this.statusMap[key].set(true);
    return observable.pipe(
     tap(() => this.statusMap[key].set(false)),
     catchError((error) => {
       this.statusMap[key].set(false);
       throw error;
     }),
    );
  }

  getSignalWatchingOnProps(keys?: KeyType[]): Signal<boolean> {
    const keySet = keys ?? this.keys;
    return computed(() => keySet.some((key) => this.statusMap[key]()));
  }
}

To implement it, start by defining a type that lists one key (string) for each operation you want to monitor:

type AnimalLoadingStatusKeyType = 'fetchCats' | 'fetchDogs' | 'fetchChickens';

Then instantiate a new LoadingStatusManager object using this type:

private readonly loadingStatusManager = new LoadingStatusManager<AnimalLoadingStatusKeyType>([
  'fetchCats',
  'fetchDogs',
  'fetchChickens',
]);

Now you can employ the loadingStatusManager object to monitor your asynchronous tasks:

fetchCats() {
  return this.loadingStatusManager.executeAndUpdateStatus(
   'fetchCats',
   this.httpClient.get('animals.org/cats'),
  );
}

fetchDogs() {
  return this.loadingStatusManager.executeAndUpdateStatus(
   'fetchDogs',
   this.httpClient.get('animals.org/dogs'),
  );
}

fetchChickens() {
  return this.loadingStatusManager.executeAndUpdateStatus(
   'fetchChickens',
   this.httpClient.get('animals.org/chickens'),
  );
}

You can easily obtain a Signal that monitors if any of the async operations are ongoing:

areAnimalsBeingLoaded: Signal<boolean> = this.loadingStatusManager.getSignalWatchingOnProps();

By not specifying any key to getSignalWatchingOnProps, it automatically watches all keys.

If you desire a Signal dedicated to specific keys, use:

areMammalsBeingLoaded: Signal<boolean> = this.loadingStatusManager.getSignalWatchingOnProps([
  'fetchCats',
  'fetchDogs',
]);

The entirety of the code for AnimalService is provided below:

import { HttpClient } from '@angular/common/http';
 import { inject, Injectable, Signal } from '@angular/core';

 import { LoadingStatusManager } from './loading-status-manager';

 type AnimalLoadingStatusKeyType = 'fetchCats' | 'fetchDogs' | 'fetchChickens';

 @Injectable({
   providedIn: 'root',
 })
 export class AnimalService {
   private readonly httpClient = inject(HttpClient);

   private readonly loadingStatusManager = new LoadingStatusManager<AnimalLoadingStatusKeyType>([
     'fetchCats',
     'fetchDogs',
     'fetchChickens',
   ]);

   readonly areAnimalsBeingLoaded: Signal<boolean> = this.loadingStatusManager.getSignalWatchingOnProps();
   readonly areMammalsBeingLoaded: Signal<boolean> = this.loadingStatusManager.getSignalWatchingOnProps([
     'fetchCats',
     'fetchDogs',
   ]);

   fetchCats() {
     return this.loadingStatusManager.executeAndUpdateStatus(
      'fetchCats',
      this.httpClient.get('animals.org/cats'),
     );
   }

   fetchDogs() {
     return this.loadingStatusManager.executeAndUpdateStatus(
      'fetchDogs',
      this.httpClient.get('animals.org/dogs'),
     );
   }

   fetchChickens() {
     return this.loadingStatusManager.executeAndUpdateStatus(
      'fetchChickens',
      this.httpClient.get('animals.org/chickens'),
     );
   }
 }
 

Any Component or Service can inject the AnimalService and access the signals from outside:

readonly animalService: inject(AnimalService);
 // ...

 this.animalService.areAnimalsBeingLoaded();

 this.animalService.areMammalsBeingLoaded();
 

This functionality can also be utilized in a Component's template employing the OnPush change detection strategy. With the assistance of Angular Signals, the template will refresh automatically whenever there are changes in the signal values.

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

How can I generate pure JavaScript, without using Typescript modules?

Take this scenario as an example ... index.ts import { x } from "./other-funcs"; function y() { alert("test"); } x(y); other-funcs.ts import { z } from "some-module"; export function x(callback: () => void): void { z(); callback(); } ...

Instructions for a safe upgrade from ngrx version 2.0 to version 4.0

Is there a direct command to upgrade from ngrx v-2 to ngrx v4 similar to when we upgraded from Angular version 2.0 to version 4.0? I have searched extensively for such a command, but all I could find in the git repository and various blogs is this npm ins ...

Collective feedback received from several HTTP requests initiated in a sequential loop

I am seeking advice on an efficient method to perform multiple HTTP requests asynchronously, and then merge all of the responses into a single array. Here is an example snippet of the code I am working with: getSamples(genes) { genes.forEach(gene ...

Navigating route parameters in Angular Universal with Java

I am currently developing a web application using Angular 5 with Server Side Rendering utilizing Angular Universal for Java. The project repository can be found here. One of the challenges I am facing is with a parameterized route defined in Angular as /pe ...

Determine if the "type" field is optional or mandatory for the specified input fields in Typescript

I need to determine whether the fields of a typescript type or interface are optional or required. export type Recommendation = { id?: string, name: string, type?: string, tt: string, isin?: string, issuer: string, quantity?: nu ...

Tips for initializing Cytoscape using Typescript

I developed a React component using Typescript that utilizes cytoscape (along with its typings) as a headless model. My goal is to turn this into an NPM package so it can be easily imported into other projects. About my library: It functions correctly wh ...

Vue.js and TypeScript combination may result in a 'null' value when using file input

I am attempting to detect an event once a file has been uploaded using a file input. Here is the JavaScript code: fileSelected(e: Event) { if ((<HTMLInputElement>e.target).files !== null && (<HTMLInputElement>e.target).files[0] !== null) { ...

Using an Object as a Key in Maps in Typescript

I had the intention of creating a Map object in Typescript where an object serves as the key and a number is the value. I attempted to define the map object in the following manner: myMap: Map<MyObj,number>; myObj: MyObj; However, when I tried to a ...

Error: When trying to run the `ng build` command in Git Bash, TypeScript module cannot be

When attempting to execute ng build using Git Bash, I encountered this error message, even though I had previously executed npm install -g typescript. Where should I place the typescript installation so that Git can detect it? Error $ npm install -g typ ...

Issues with command functionality within the VS Code integrated terminal (Bash) causing disruptions

When using Visual Studio Code's integrated terminal with bash as the shell, I have noticed that commands like ng and tsc are not recognized. Can anyone shed some light on why this might be happening? ...

Retrieving the previous and current URL in Angular 8

Need help setting variables prevUrl and currentUrl in Angular 8 by fetching previous and current URLs. The scenario involves two components - StudentComponent and HelloComponent. When transitioning from HelloComponent to StudentComponent, I face an issue. ...

A guide to properly executing initialization functions in Angular with Electron

Currently, I am working with Angular and Electron. I have a Preferences object that I need to initialize (Preferences.init()) before any other code is executed. Is there a specific location in Angular or Electron where this initialization code should be pl ...

Discovering React Styled Components Within the DOM

While working on a project using Styled Components in React, I have successfully created a component as shown below: export const Screen = styled.div({ display: "flex", }); When implementing this component in my render code, it looks like this ...

Count the number of checked checkboxes by looping through ngFor in Angular

My ngFor loop generates a series of checkboxes based on the X number of items in childrenList: <div *ngFor="let child of childrenList; let indice=index"> <p-checkbox label="{{child.firstname}} {{child.lastname}}" binary=&qu ...

The type 'MockStoreEnhanced<unknown, {}>' is not compatible with the type 'IntrinsicAttributes & Pick[...]

I'm currently working on creating a unit test for a container in TypeScript. From what I've gathered from various responses, it's recommended to use a mock store and pass it to the container using the store attribute. This method seems to o ...

How can I access keys and values from an Observable within an Angular template?

Attempting to display all keys and values from an Observable obtained through Angular Firebase Firestore Collection. This is how I establish a connection to the collection and retrieve an Observable. The function is called subsequently. verOrden : any; ...

What is the best way to retrieve the `any` type when utilizing the `keyof` keyword?

I am struggling to articulate this question properly, so please refer to the code below interface TestParams<T> { order?: keyof T attr1?: number attr2?: string } async function Test<T = any>(_obj: TestParams<T>): Promise<T> { ...

What is the best way to bring a string into my .tsx file using a relative reference from a module?

I am currently developing an online course on creating a website using StencilJS, NodeJS, and the IonicFramwork. As a newcomer in this field, I have encountered a challenging issue: In my project, the API "https://swapi.dev/api" is imported as a ...

Tips for transferring parameters between components: leveraging the ? and & operators within URLs

Is there a way to pass parameters between components other than using ActivatedRoute? Currently, I am utilizing ActivatedRoute. In component1.ts this.router.navigate(['/rate-list', 1]); In app.module.ts { path: 'rate-list/:a', pathM ...

Revamp the button's visual presentation when it is in an active state

Currently, I'm facing a challenge with altering the visual appearance of a button. Specifically, I want to make it resemble an arrow protruding from it, indicating that it is the active button. The button in question is enclosed within a card componen ...