1

I am refactoring some angular code that subscribed to data from a service call. Instead I want the data to be returned from the service as an observable so I can utilize the async pipe so I do not have to unsubscribe from everything at the end. My old code looked something like this:

Component.ts

people: Person[] = [];

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

Then elsewhere I can access the data using this.people to look through the data and do whatever I need to.

Now I want to do something like the following. But if I do, how do I access the same data within the component without subscribing to the data and having to unsubscribe on ngOnDestroy

Component.ts

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

And in my service:

ApiService.ts

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

With the people$ now being an observable I can do the following in my html:

Index.html

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

From what I understand, doing that will automatically unsubscribe as needed. But now that I have the data in the component as people$ which is an observable, how can I access the data? For example, I have an observable of cities and I want to filter them based on whether the observable people are marked with display = true. I cannot do something like below. So what is the best way to get the same 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 end result would be a list of cities that are only in the list of people's city propery where the display = true. Is there a way to do this without subscribing? Because if I subscribe, doesn't it defeat the purpose of the async pipe in the first place?

UPDATE If I have the two observables cities$ and people$, how do I update the cities$ based on changes to people$ ?. For example if the user updates one of the people to have the display = false instead of true, I would want to filter out that record from the cities$.

Example

updatePerson(person: Person){
 this.apiService.updatePerson(person);
//update this.cities$ to filter 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));
}

2 Answers 2

1

You can .pipe() this.people$ and use .tap() operator.

Like this:

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

Now this logic is triggered when the async pipe is called on people$ in the template.

Update: Use .combineLatest()

//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>
1
  • Thank you. How do I incorporate the other observable into this logic? I have two observable lists and want to update one when the other updates? For example using my scenario above, if the user changes one of the people to have their display be false instead of true I want that to also update the cities observable at the same time.
    – Ben
    Commented Jan 30, 2024 at 20:21
1

There is nothing wrong with subscribing to observables in the component alongside using pipe async in the template, the idea of async pipe is that Angular will handle the unsubscription for you.

You don't always need to explicitly call unsubscribe, there are more elegant way of ensuring the stream doesn't stay alive and cause memory-leak.

For example you can use operator like take(1) or first() to make sure the stream doesn't stay alive after emitting the first value, this is useful when you need one-time access to the value emitted from the stream, like sending an http request.

Also, you can (and actually should) add operator like takeUntil to your streams that will stay alive for a while and link it with your component ngOnDestroy life-cycle, check here, or the new takeUntilDestroyed operator.

So first approach is simple buildup of streams without needing to subscribe in the component itself:

  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))
    )
  );

And in case you found your self accessing the last emitted value of the stream frequently and because you are already using BehaviorSubject you can get the last value using 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)
  );
}

Another approach is to use Angular Signals which is available starting from 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));
});

Full example on Stackblitz

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.