Retrieving Angular observable data from a component with the assistance of the async pipe

Currently, I am in the process of refactoring some Angular code that previously involved subscribing to data from a service call. My goal now is to have the data returned from the service as an observable so that I can make use of the async pipe and avoid manual unsubscription at the end. My previous code snippet looked something like this:

Component.ts

people: Person[] = [];

getPeople(){
  this.apiService.GetPeople().subscribe((result) => {
  this.people = result;
 }));
}

Subsequently, I could access the data using this.people to manipulate it as needed.

Now, I aim to restructure my code by utilizing observables. However, if I proceed with this approach, how can I access the same data within the component without the need for subscription and subsequent unsubscription on ngOnDestroy?

Component.ts

people$ = Observable<Person[]>;
getPeople(){
 this.people$ = this.apiService.people$;
}

The corresponding code in my service layer would be:

ApiService.ts

private allPeople: BehaviorSubject<Person[]> = new BehaviorSubject<Person[]>([]);
people$ = this.allPeople.asObservable();

With people$ being converted into an observable, I can now utilize the following in my HTML:

Index.html

<div *ngIf="people$ | async">
</div>

As per my understanding, this approach handles unsubscribing automatically when required. With people$ being an observable in the component, how can I retrieve and work with the data? For instance, given an observable of cities, how do I filter them based on whether the observable people are marked with display = true? The conventional method does not seem feasible. What would be the best way to achieve similar functionality?

Component.ts

let peopleToUse = this.people$.filter(m => m.display === true).map((filter) => {return filter.city});
this.cities$ = this.cities$.filter(m => peopleToUse.includes(m.city));

The desired outcome is to obtain a list of cities that exist in the list of people's city property where display = true. Is there a means of achieving this without direct subscription? After all, wouldn't subscribing contradict the purpose of employing the async pipe initially?

UPDATE In case I have two observables namely cities$ and people$, how can I update cities$ according to changes in people$? For example, if the user modifies one individual in people$ such that display = false, I intend to remove that record from cities$.

Illustration

updatePerson(person: Person){
 this.apiService.updatePerson(person);
//Update this.cities$ to implement filtering once again.
let peopleToUse = this.people$.filter(m => m.display === true).map((filter) => {return filter.city});
this.cities$ = this.cities$.filter(m => peopleToUse.includes(m.city));
}

Answer №1

To enhance the functionality, you have to use the .pipe() method on this.people$ and incorporate the .tap() operator.

Here is an example:

people$ = Observable<Person[]>;
getPeople() {
    this.people$ = this.apiService.people$.pipe(
      tap((people) => {
        <your logic here>
      })
    );
  }

This way, the specified logic will be executed when the async pipe is activated on people$ in the template.

Additionally, consider utilizing the .combineLatest() function for better efficiency.

//component.ts
   ngOnInit(): void {
    // Assuming you have services that provide these observables
    this.people$ = this.peopleService.getPeople();
    this.cities$ = combineLatest([this.people$, this.cityService.getCities()])
      .pipe(
        map(([people, cities]) => {
          const updatedCities = cities.filter(m => people.includes(m.city));
          return updatedCities;
        })
      );
  }
component.html
<div *ngIf="(people$ | async) as people">
  <!-- Display people list -->

  <div *ngFor="let person of people">
    {{ person.name }}
  </div>
</div>

<div *ngIf="(cities$ | async) as cities">
  <!-- Display cities list -->

  <div *ngFor="let city of cities">
    {{ city.name }}
  </div>
</div>

Answer №2

It is perfectly fine to subscribe to observables in the component while also using the async pipe in the template. The purpose of the async pipe is for Angular to handle the unsubscription process for you automatically.

You don't always have to call unsubscribe manually, as there are more elegant ways to ensure that the stream doesn't linger and cause memory leaks.

One approach is to use operators like take(1) or first() to ensure that the stream stops after emitting the first value. This can be helpful when you only need access to the initial emitted value, such as when making an HTTP request.

Additionally, adding operators like takeUntil to your streams that may remain active for a period of time and connecting it with the component's ngOnDestroy lifecycle hook is recommended. You can also explore the new takeUntilDestroyed operator.

Another option is to leverage Angular Signals, introduced in Angular 16:

readonly #people = toSignal<Person[]>(this.#peopleService.people$) as Signal<
  Person[]
>;
readonly #cities = toSignal(this.#peopleService.cities$) as Signal<string[]>;

protected filteredCities = computed(() => {
  const displayPeopleCities = this.#people().reduce<string[]>((acc, cur) => {
    if (cur.display === true) {
      acc.push(cur.city);
    }

    return acc;
  }, []);

  return this.#cities().filter((city) => displayPeopleCities.includes(city));
});

Here is a simple example of setting up streams without direct subscription in the component:

  readonly #peopleService = inject(PeopleService);
  readonly displayPeopleCities$ = this.#peopleService.people$.pipe(
    takeUntilDestroyed(),
    map((people) =>
      people.reduce<string[]>((acc, cur) => {
        if (cur.display === true) {
          acc.push(cur.city);
        }

        return acc;
      }, [])
    )
  );
  readonly filteredCities$ = combineLatest([
    this.#peopleService.cities$,
    this.displayPeopleCities$,
  ]).pipe(
    takeUntilDestroyed(),
    map(([cities, displayPeopleCities]) =>
      cities.filter((city) => displayPeopleCities.includes(city))
    )
  );

If you find yourself frequently needing to access the last emitted value of a stream and already using a BehaviorSubject, you can retrieve the last value using the getValue method on the BehaviorSubject:

// people.service.ts

getPeopleSnapshot(): Person[] {
  return this.#peopleChange.getValue();
}

getCitiesSnapshot(): string[] {
  return this.#citiesChange.getValue();
}


// component.ts

ngOnInit(): void {
  this.#peopleService.people$
    .pipe(takeUntilDestroyed(this.#destroyRef))
    .subscribe(() => this.filterCities());
}

private filterCities(): void {
  const people = this.#peopleService.getPeopleSnapshot();
  const cities = this.#peopleService.getCitiesSnapshot();
  const displayPeopleCities = people.reduce<string[]>((acc, cur) => {
    if (cur.display === true) {
      acc.push(cur.city);
    }

    return acc;
  }, []);

  this.filteredCities = cities.filter((city) =>
    displayPeopleCities.includes(city)
  );
}

For a complete example, check out this Stackblitz demo.

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

Angular 6 - ngModel Value Reveals Itself upon User Interaction

I am currently working on a component that lists items with a dropdown option to change values. However, I have noticed a small issue where the selected item in the dropdown appears empty upon component creation. The selection only becomes visible after cl ...

Assigning a value and specifying the selected attribute for an options tag

Trying to understand the challenge of setting both a value and a selected attribute on an options tag. Each one works independently, but not together. For example: <select> <option *ngFor="let item of items" selected [ngValue]="item"> ...

Instructions for incorporating a TypeScript type into a Prisma model

I am trying to incorporate a Type Board into one of my Prisma models. However, I am encountering an error stating that "Type 'Board' is neither a built-in type, nor refers to another model, custom type, or enum." Can someone provide guidance on h ...

Encountering issue with npm installing incorrect version of angular-cli

I need to install a specific version of Angular, specifically 8.3.19. To do so, I executed the command npm install -g @angular/<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="5f3c33361f67716c716e66">[email protected]< ...

Is it possible to have a TypeScript Reducer alongside JavaScript Reducers in my combineReducers function?

export default combineReducers({ todosReducer, calculatorReducer, dateReducer, }); I've encountered a challenge while trying to incorporate TypeScript into a portion of my extensive codebase. In the code snippet above, envision the first two reducers ...

What is the reason for having a filled array containing data on currency exchange rates within an asynchronous function, while simultaneously having an empty array located outside of it? - React

After struggling with this programming issue for the past 5 days, I'm reaching out for help. The challenge I'm facing involves retrieving the rate of a specific currency between 1 and 50000 from an API. While I have successfully managed to obtai ...

Is it possible for my commitment to consistently provide identical values, even when the data varies each time it is invoked?

Initially, the getCart method is invoked in b-navbar.component.ts: export class BNavbarComponent implements OnInit{ appUser: any; cart$ : Observable<ShoppingCart | null>; constructor(private auth : AuthService, private shoppingCartService : S ...

Error Alert: missing property data in angular 5 type

I attempted to design an interface in interface.ts. The data consists of an array of objects inside the Column. Below is the code for my interface: export class User { result: boolean; messages: string; Column=[]; data=[]; } export class Column { name ...

Grouping JavaScript nested properties by keys

I have a specific object structure in my code that I need to work with: { "changeRequest": [{ "acrId": 11, "ccrId": "", "message": "test message" }, ...

There is an issue with the property 'updateModf' in the constructor as it does not have an initializer and is not definitely assigned

When working with an angular reactive form, I encountered an issue. After declaring a variable with the type FormGroup like this: updateModf:FormGroup; , the IDE displayed an error message: Property 'updateModf' has no initializer and is not def ...

Improving Angular's Performance by Handling Absence of Providers for $rootScope and $location

I successfully set up Angular Hybrid configuration using Angular upgrade to enhance performance and the application is running smoothly. However, I am encountering an issue with running test cases on the Angular side. I have provided all the necessary com ...

An unexpected error occurs when attempting to invoke the arrow function of a child class within an abstract parent class in Typescript

Here is a snippet of code that I'm working on. In my child class, I need to use an arrow function called hello(). When I try calling the.greeting() in the parent class constructor, I encounter an error: index.ts:29 Uncaught TypeError: this.hello is ...

No input in ng2-select2

How can I reset the value in ng2-select2 by clicking on a button? Here is my current HTML code: <select2 id="bill" [data]="colBill" [options]="option" (valueChanged)="onBillArray($event);"> The corresponding Typescript code is: this.arrBill = []; ...

Updating a subscribed observable does not occur when pushing or nexting a value from an observable subject

Help needed! I've created a simple example that should be working, but unfortunately it's not :( My onClick() function doesn't seem to trigger the console.log as expected. Can someone help me figure out what I'm doing wrong? @Component ...

Adding HTTP headers to responses in Angular2: A guide to customizing the headers sent from your app to the browser

Currently, I am in the process of prototyping an Angular2 application while at work. The application needs to operate under specific company middleware that requires various Link headers to be included in responses received from the Angular2 app. Unfortuna ...

Struggling to update minimist in Angular 9?

Lately, I've been working on a project using Angular 9 and came across a warning about a security vulnerability related to the minimist package. Despite my efforts to fix it with "(sudo) npm audit fix" or updating with "(sudo) npm update", the issues ...

Tips for keeping a mat-checkbox selected across page transitions in Angular

Check out my custom mat-checkbox code snippet: <mat-checkbox (click)="$event.stopPropagation()" (change)="$event ? selection.toggle(element) : null" [checked]="selection.isSelected(element)"> </mat-checkbox> ...

The newDragSource() function is not functioning properly within golden-layout version 2.6.0

We are currently in the process of migrating from golden-layout version 1.5.9 to version 2.6.0 within a large Angular 16 production application, albeit slowly and somewhat painfully. Within our application, there exists a dropdown menu that displays the n ...

What are the steps involved in creating a definition file for npm?

Is there a recommended way to include a definition file (.d.ts) into an npm module as outlined in this guide? Specifically, how can I reference the node without using \\\ <reference /> due to restrictions? ...

Is it possible to nest forkJoins within another forkJoin call?

I have implemented a route resolver in this manner, where I want to make two additional HTTP requests based on the initial response from forkjoin. I attempted to nest another forkjoin inside the first one, but I am open to suggestions on better approache ...