5

I'm trying to use the async/await (for the first time) and I can't wrap my head around it.

What am I doing wrong here:

tree.component.ts

export class TreeComponent implements OnInit {

  private routeSub: any;
  acronym: string;
  flare: any[];

  constructor(
    private location: Location,
        private route: ActivatedRoute,
        private http: HttpClient,
        private router: Router,
        private fileTreeService: FileTreeService
  ) {


  }
  ngOnInit(): void {
      this.routeSub = this.route.params.subscribe(
        params => {
            this.acronym = params.name
            this.fileTreeService.getFileTree(params.name).subscribe(item => {
                this.flare = item;

            });
        });

filetree.service.ts

export class FileTreeService {

    res: any;

    constructor(private http: HttpClient) { }



    async getFileTree(acronym) {
        const headers = new HttpHeaders()
        this.res = await this.http.get<any>(`${IP}/filetree/tree/`, { headers });
        return this.res;
    }
}

I'm getting the error "Property 'subscribe' does not exist on type 'Promise'" in filetree.component.ts. I've reached the end of the road here, so I'm reaching out to you guys. Thanks in advance.

Update:

Thanks for the help, it kinda worked but it didn't do what I was trying to achive. Isn't supposed to wait for the result before continue executing the code?

  ngOnInit(): void {
      this.routeSub = this.route.params.subscribe(async (params) => {
            this.acronym = params.name
            this.flare = await this.fileTreeService.getFileTree(params.name).toPromise();
            console.log("flare = ", this.flare);
        });

    let test = this.flare;
    console.log("test = ", test);
} 

root is declared before flare has been given a value. This what my browser console spits out.

test =  undefined
flare =  {name: "data", children: Array(81)}
BuZZ-dEE
  • 6,075
  • 12
  • 66
  • 96
klabbaparn
  • 157
  • 3
  • 9
  • Async functions are expected to return a promise. Subscribe is for observables usually. getFileTree doesn't return an observable – Phix Aug 24 '20 at 20:35
  • I see.. how do I return a promise instead? Guess it's more complicated than I first thought. – klabbaparn Aug 24 '20 at 20:41
  • You're already returning a promise. Instead of `subscribe` use `then` in `ngOnInit`. – Phix Aug 24 '20 at 20:42

2 Answers2

6

async/await work on promise so in case you use httpClinet methods you need to convert the return value from Observable to promise by toPromise method

filetree.service.ts

export class FileTreeService {
    res: any;

    constructor(private http: HttpClient) { }

    getFileTree(acronym) {
        const headers = new HttpHeaders()
        // return an observable 
        return  this.http.get<any>(`${IP}/filetree/tree/`, { headers })    
   }
}

component

  ngOnInit(): void {
      //  mark the upper function with async
      this.routeSub = this.route.params.subscribe(async (params) => {  
        this.acronym = params.name;
        //  now we can use await but we need to convert the observable to promise 
        this.flare = await this.fileTreeService.getFileTree(params.name).toPromise();
        });
}

you can read in depth tutorial here

Updated

we can mark ngOnInit with async and use await for all observances after we convert theme to promise in that case we will no longer to use subscribe.

  //  mark the upper function with async
  async ngOnInit(): Promise<void> {
    //  now we can use await but we need to convert the observable to promise 
    this.acronym = (await this.route.params.toPromise()).name;
    this.flare = await this.fileTreeService.getFileTree(this.acronym).toPromise();
    let test = this.flare;  // => {name: "data", children: Array(81)}
    console.log("test = ", test); // => {name: "data", children: Array(81)}

  }
Muhammed Albarmavi
  • 23,240
  • 8
  • 66
  • 91
  • 1
    I'm trying to follow your lead here, but I'm probably missing something here. It still doesn't wait for the return value before proceeding. Added an update in my post. – klabbaparn Aug 24 '20 at 21:13
  • I cannot say that this is wrong: ngOnInit(): void { // mark the upper function with async this.routeSub = this.route.params.subscribe(async (params) => { this.acronym = params.name; // now we can use await but we need to convert the observable to promise this.flare = await this.fileTreeService.getFileTree(params.name).toPromise(); }); } But i can tell you for sure that you can do a subscribe inside of subscribe, so you can just subscribe to the fileTreeService and do there what you want. – Erik Aug 24 '20 at 21:25
  • @Erik there no wrong of using async/await to prevent nested subscription I will update my answer to use await for all observables in ngOnInit – Muhammed Albarmavi Aug 24 '20 at 21:38
  • @klabbaparn check my updated answer where I use await for all observables – Muhammed Albarmavi Aug 24 '20 at 21:46
2

If you are new to async/reactive programming, I wouldn't recommend you to mix promises and observables. Since you're using Angular HttpClient Module, which returns an observble for an HTTP request, we could stick only with it.

So you could return the HTTP request directly without converting it to a promise. Sidenote: observables also provide much more control on the data flow and data transform operators compared to promises.

Service

export class FileTreeService {
  res: any;

  constructor(private http: HttpClient) { }

  getFileTree(acronym): Observable<any> {      // <-- `http.get()` returns an observable
    const headers = new HttpHeaders();
    return this.http.get<any>(`${IP}/filetree/tree/`, { headers });
  }
}

Additionally, now you have a child observable from this.fileTreeService.getFileTree() function that depends on the notification from another observable this.route.params.

In such cases instead of having multiple inner subscriptions like you're doing, you could use RxJS higher order mapping operator like switchMap to map from one observable to another. Now 2 different data streams from your code is reduced to one single stream. Lesser the streams - lesser the potential memory leaks.

Component

import { Subject } from 'rxjs';
import { switchMap, takeUntil } from 'rxjs/operators';

export class TreeComponent implements OnInit, OnDestroy {
  private routeSub: any;
  acronym: string;
  flare: any[];
  complete$ = new Subject<any>();     // <-- use to close open subscriptions

  constructor(
    private location: Location,
        private route: ActivatedRoute,
        private http: HttpClient,
        private router: Router,
        private fileTreeService: FileTreeService
  ) { }
 
  ngOnInit(): void {
    this.routeSub = this.route.params.pipe(
      switchMap(params => {           // <-- use `switchMap` to map from one observable to another
        this.acronym = params.name;
        return this.fileTreeService.getFileTree(params.name);
      }),
      takeUntil(this.complete$)       // <-- close the subscription when `this.complete$` emits
    ).subscribe(
      item => {
        this.flare = item;
        console.log(this.flare);      // <-- correct log: will print the value of `this.flare`
      },
      error => {
        // always good practice to handle errors from HTTP requests
      }
    );

    console.log(this.flare);          // <-- wrong log: will print `undefined`
  }

  ngOnDestroy() {
    this.complete$.next();            // <-- close open subscriptions
  }
}

Also remember, since the this.flare variable is assigned asynchronously, all statements that depend on it must be inside the subscription. It is illustrated in the code above using the different console.log() statements. The one inside subscription will print the correct value whereas the one outside the subscription will wrongfully print either undefined or older value held by the this.flare.

I've also used RxJS takeUntil operator to close open subscriptions in the ngOnDestory() hook. You could find more details about it and other easier method of handling unsubscriptions here.

ruth
  • 29,535
  • 4
  • 30
  • 57