How to make program wait until observable is executed in Angular
Asked Answered
C

5

9

I'm working on an angular project, I have a situation where making a call to backend using an observable to fetch products.

Here is how the code looks like.

getProducts () : Product[] {
this.http.get<[]>(this.m_baseURL+'/products').subscribe(res => {
  console.log(res)
  this.products = res;
});
  return this.products
}

Problem is, the return statement doesn't wait for the above statement to get executed. And in my component, I get an empty array. I checked using console log in both service and component. And it turns out return statement gets executed before observable is done assigning the value.

How do I make it stop until it completes its job, just like async await does. Should I use normal async await instead of observable?

This is my first Angular project, so please pardon me if the question is novice.

Counterword answered 24/4, 2022 at 14:43 Comment(2)
Does this answer your question? How do I return the response from an Observable/http/async call in angular?Daegal
Thanks for the suggestion. But I'm afraid that doesn't answer my question.Counterword
C
7

Instead of waiting for returns to time some tasks use reactive approach.

SomeService.ts

products$: Product[];

getProducts() : Product[] {
   this.products$ = this.http.get<[]>(this.m_baseURL+'/products');
}

SomeComponents.ts

filteredProducts: Product[];
private readonly unsubscribe$ = new Subject();

constructor(private someService: SomeService){}

ngOnInit() {
   this.someService.getProducts();

   this.someService.products$.pipe(takeUntil(this.unsubscribe$)).subscribe((products) => {
   this.filteredProducts = products.filter(product => product.id > 0); // look over filtering
});
}

ngOnDestroy() {
 this.unsubscribe$.next();
}

SomeComponent.html

<div *ngFor="product of filteredProducs">{{ product }}</div>

Many ways to approach this common problem. Many ways to improve it as well. This is one way. I don't know how your filtering works, but if possible I would prefer to use | async pipe to avoid manual subscribing at all and filter with an additional pipe or filter the observable itself.

Carlist answered 25/4, 2022 at 3:14 Comment(0)
D
1

To wait for the first value from an observable you can use firstValueFrom(). This method is dangerous since it can cause your application to hang indefinitely, always include a timeout mechanism.

  async getProducts(): Promise<Product[]> {
    const res = await firstValueFrom(
      this.http.get<[]>(this.m_baseURL + '/products').pipe(timeout(10000))
    );
    console.log(res);
    this.products = res;
    return this.products;
  }

A better design is to instead only make the http request to update this.products, and just use the variable like it always has content, you can do some other things in your subscription if you want side effects to happen on update.

products: Product[] = [];

ngOnInit(){
  this.updateProducts();
}

updateProducts() : void {
  this.http.get<[]>(this.m_baseURL+'/products').subscribe(res => {
    console.log(res)
    this.products = res;
    this.doSomething();
    this.doSomethingElse();
  });
}

Change detection will automatically update your html when an http response is received, but you can always force it with ChangeDetectorRef.detectChanges().

<div *ngFor="product of products">{{ product.name }}</div>

The above html will be blank until your first subscription completes, at which point it should automatically update.


If you're using a service it would look like this:

service

products: Product[] = [];

updateProducts() : void {
  this.http.get<[]>(this.m_baseURL+'/products').subscribe(res => {
    console.log(res)
    this.products = res;
    this.doSomething();
    this.doSomethingElse();
  });
}

component

constructor(private service: MyService){}

ngOnInit(){
  this.updateProducts();
}

get products(){
  return this.service.products;
}

updateProducts(){
  this.service.updateProducts();
}

The two wrapper methods are just so you don't have to write service in your html.


If you want unique side effects to happen in different components, you would convert this.products to a subject, and then you can subscribe and execute a callback whenever products changes. A BehaviorSubject lets you initialize the value to an empty array.

service

products$ = new BehaviorSubject<Product[]>([]);

updateProducts() : void {
  this.http.get<[]>(this.m_baseURL+'/products').subscribe(res => {
    console.log(res)
    this.products$.next(res);
  });
}

If you want to save the value of the subject in the component rather than subscribing with the async pipe, make sure to unsubscribe when the component is destroyed. That's because this subject does not complete like the observables from HttpClient, so subscriptions will remain in memory unless unsubscribed.

component

sub = new Subscription();
products = [];

constructor(private service: MyService){}

ngOnInit(){
  this.sub = this.service.products$.subscribe(res => {
    console.log(res)
    this.products = res;
    this.doSomething();
    this.doSomethingElse();
  });
  this.updateProducts();
}

updateProducts(){
  this.service.updateProducts();
}

ngOnDestroy(){
  this.sub.unsubscribe();
}
Dara answered 25/4, 2022 at 5:4 Comment(0)
U
0

You could use an async / await pattern for this, but I recommend against it. Learn how to program asyncronously using Observables.

The service method to load products should return the Observable:

getProducts () : Observabe<Product[]> {
 return this.http.get<[]>(this.m_baseURL+'/products')
}

The consuming code should subscribe to the Observable and get the data:

myGetProductService.getProducts().subscribe(res => {
  console.log(res)
  this.products = res;
});
Unchurch answered 24/4, 2022 at 15:5 Comment(4)
Agree with first part, strongly disagree with second. Subscribing to an Observable to store what it returns in a local variable is an Angular anti-pattern.Americaamerican
@WillAlexander then how should I handle this situation? I want to fetch all products once, store them in service i.e. products array. Now if I want to filter specific products i.e. by name, id, category, I simply get them by iterating the products array rather than calling API methods for each filter? Please have a look at this scenarioCounterword
Look at Redux/store pattern. You can use a BehaviorSubject to “store” the latest value in a way which is still 100% Observable-based. You don’t have to use a big library like NgRx, but if your app is growing, it can really be worthwhileAmericaamerican
"Subscribing to an Observable to store what it returns in a local variable is an Angular anti-pattern. " Most of that time it's all that we need with no need for additional complexity.Unchurch
A
0

The Angular way of doing things here is to not return a Product[], but to return the Observable itself:

getProducts(): Observable<Product[]> {
  return this.http.get<Product[]>(this.m_baseURL+'/products');
}

You then use the async pipe to subscribe to this Observable, and Angular will automatically display the data.

That's for the case where this is a service method. If you're doing this in a component, the simple answer is: don't. Put this in a service. That's what they're for.

Strongly suggest you try Angular's Tour of Heroes tutorial. They go over this sort of thing.

Americaamerican answered 24/4, 2022 at 15:6 Comment(1)
What I'm trying to do is store the products in service as I fetch them. So that when I use filters to search the specific products, I may utilize the products stored in array within the service, instead of calling API methods for each filter. Thus reducing number of API calls.Counterword
E
0

Angular Does Not Support Synchronous Observables or HttpClient Calls

BAD NEWS!! Angular does not support any type of true synchronous calls!

This confuses new Angular developers who want to wait for data to be retrieved in Angular, but there is no way to delay an Observable type in Angular as its build around await-async technology. The same goes for HttpClient, Promise, or Fetch which are all await-async type constructs.

The only way to make a truly synchronous request is using the old XMLHttpRequest() synchronous call for data, which still can be used in Angular. But that is not a good choice in an asynchronous technology like Angular.

    function MyXMLHttpRequest() {
      let req = new XMLHttpRequest();
      req.open('GET', "https://example.com/data.json");
      req.onload = function() {
        if (req.status == 200) {
          alert("Success : "+this.responseText);
        } else {
          alert("Error : Status Error : " + req.status);
        }
      }
      req.send();
    };
    MyXMLHttpRequest();

Read on...

What many Angular programmers get confused about is whether Observables can be tricked into acting synchronous (wrapped by async-await promises, etc). But they cannot. Many developers think adding or removing timers, delays, or callback functions changes their basic asynchronous behavior, but they do not. A Synchronous process means a thread or a call to a process that stops or blocks all outside code from continuing to run until the process is complete (like traditional JavaScript). Observables, HttpClient, and Promises are all asynchronous objects.

Asynchronous code runs in its own timeline, which means after the async code is triggered, JavaScript jumps down to the next code block code AFTER the Observable or async code and keeps running. You cannot stop that. Once the Observable returns with its data, the script jumps back to the Observable to complete its processing, then returns to where it left off before.

Other than using a truly old-fashioned call to XMLHttpRequest(), your best strategy is to wrap the Observable into a deadend angular method like a custom function, ngOnInit(), the component's constructor, or a routing guard and add on a callback method at the end of its data processing pipeline that jumps to your synchronous code. That guarantees your code does not run until the data processing Observable is done. That then forces the Angular Observable to pause and gather all its data and complete its events before calling the next part of your code. And this simulates a synchronous code event using Observables. Below is an example of that.

Fake a Synchronous Call in Angular

You can fake a synchronous call by using a simple callback function in the subscription area of your Observable. The custom function in the Angular Component below runs synchronously after the Observable.The doSomethingNew() method will not run until the HttpClient (an Observable type) subscription completes its gathering of data and assigns it to the products property:

import {HttpClient} from '@angular/common/http';

export class MyComponent {

  products: IProduct[] = [];

  constructor(private http: HttpClient){

      this.http.get<IProduct[]>('api/products/products.json').subscribe(
          data => {
            this.products = data;
            // This will not fire till all the data is loaded...
            this.doSomethingNew();
          },
          error => {console.error(error)}
      );
  }

  // This does not get called till the HttpClient (Observable) is completed!
  // This simulates a synchronous routine.
  // Notice our products array now has the data we need.
  doSomethingNew(){
    let mynewdata = this.products;
    // do something with mynewdata
  }
}
Emplacement answered 14/5, 2023 at 9:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.