Home > Blockchain >  How to make a function which relies on a previous functions data wait until that data is set
How to make a function which relies on a previous functions data wait until that data is set

Time:04-20

Problem: I'm attempting to retrieve information from Firebase with a second function which relies on data that was set from a prior function call. However, the second function executes before the first has set the data. I know this is because of how functions execute in Typescript / Angular, but i'm not very familiar working with asynchronous functions.

Question: What is the best way to make the second function wait until the data it needs is set?

Additional Info: The data is being stored / retrieved from Firebase Firestore. The collection im working with contains any number of city documents. Each of these city documents contains a collection of families. Since the number of cities can vary, I need to first retrieve a list of cities that have families, then use that list to retrieve the families inside. I've attempted to use Promises to fix the issue, tried making the functions asynchronous (then using await), and making callback functions, but have had no luck. I've included my attempted solutions below. I there is any more code that I need to include (or if I need to post the Firestore layout) please let me know.

I'm also open to other solutions in retrieving / storing the data as long as it follows the same format as the Firestore data.

Code:

home.component.ts:

export class HomeComponent implements OnInit {
  activeCities: any = [];
  activeFamilies: Map<string, Family[]>;

  constructor(private fbService: FirebaseService) { }

  ngOnInit() {
    this.getActiveCities();
    this.getAllFamilies();
  }

  getActiveCities() {
    this.fbService.getActiveCities().subscribe(
      data => {
        this.activeCities = data;
      },
      error => {
        console.log("Error retrieving active cities");
      }
    );
  }

  getAllFamilies() {
    for (let city of this.activeCities) {
      this.fbService.getFamiliesForCity(city.id).subscribe(
        data => {
          let families: Family[] = [];
          families = data;
          this.activeFamilies.set(city .id, families);
        },
        error => {
          console.log("Error retrieving families for active cities");
        }
      );
    }
  }
}

firebase.service.ts:

export class FirebaseService {
  private activeCitiesPath = '/active_cities';
  constructor(private firestore: AngularFirestore) { }

  getActiveCities() {
    let colRef: AngularFirestoreCollection<any>;
    let temp: Observable<any[]>;
    let path = this.activeCitiesPath;
    colRef = this.firestore.collection(path);
    return colRef.snapshotChanges().pipe(
      map(actions => actions.map(a => {
        const data = a.payload.doc.data() as any;
        const id = a.payload.doc.id;
        return { id, ...data };
      }))
    );
  }

  getFamiliesForCity(cityCode: string) {
    let colRef: AngularFirestoreCollection<any>;
    let temp: Observable<any[]>;
    let path = this.activeCitiesPath   "/"   cityCode   "/families";
    colRef = this.firestore.collection(path);
    return colRef.snapshotChanges().pipe(
      map(actions => actions.map(a => {
        const data = a.payload.doc.data() as any;
        const id = a.payload.doc.id;
        return { id, ...data };
      }))
    );
  }

}

Attempted solutions: I've tried the following solutions but neither have worked thus far:

With promises:

async ngOnInit() {
    let promises: Promise<void>[] = [];
    promises.push(this.getActiveCities());
    promises.push(this.getAllFamilies());

    Promise.all(promises).then(() => {
      console.log("All promises worked");
    }).catch(() => {
      console.log("Error in promise");
    });
}

private getActiveCities(): Promise<void>  {
    return new Promise<void>((resolve, reject) => {
        //same code but adding resolve(); and reject();
    });
}

private getAllFamilies(): Promise<void>  {
    return new Promise<void>((resolve, reject) => {
        //same code but adding resolve(); and reject();
    });
}

With asynchronous:

async ngOnInit() {
    await this.getActiveCities();
    await this.getAllFamilies();
}

With callbacks I attempted something similar to: https://stackoverflow.com/a/21518470/5785332

I've also tried to implement solutions from answers to similar question: async/await in Angular `ngOnInit`

CodePudding user response:

I cannot speak to the best angular design here. However to implement this with promises, the following should work. I went ahead and made both methods awaitable.

export class HomeComponent implements OnInit {
  activeCities: any = [];
  activeFamilies = new Map<string, Family[]>();

  constructor(private fbService: FirebaseService) {}

  ngOnInit() {
    this.getActiveCities().then(() => {
      this.getAllFamilies();
    });
  }

  getActiveCities() {
    return new Promise<void>((resolve, reject) => {
      this.fbService.getActiveCities().subscribe(
        (data) => {
          this.activeCities = data;
          resolve();
        },
        (error) => {
          console.log('Error retrieving active cities');
          reject(error);
        }
      );
    });
  }

  getAllFamilies() {
    const promises = Array<Promise<void>>();

    for (let city of this.activeCities) {
      promises.push(
        new Promise<void>((resolve, reject) => {
          this.fbService.getFamiliesForCity(city.id).subscribe(
            (data) => {
              this.activeFamilies.set(city.id, data);
              resolve();
            },
            (error) => {
              console.log('Error retrieving families for active cities');
              reject(error);
            }
          );
        })
      );
    }

    return Promise.all(promises);
  }
}

interface OnInit {
  ngOnInit(): void;
}

interface Family {}

interface FirebaseService {
  getActiveCities: () => {
    subscribe: {
      (success: (data: any[]) => void, error: (error?: any) => void): void;
    };
  };
  getFamiliesForCity: (cityCode: string) => {
    subscribe: {
      (success: (data: any[]) => void, error: (error?: any) => void): void;
    };
  };
}

I'm not sure if you can make ngOnInit async either without causing problems but it then becomes.

async ngOnInit() {
  await this.getActiveCities();
  await this.getAllFamilies();
}

CodePudding user response:

Here is an easy solution using rxjs lastValueFrom.

lastvalueFrom: Converts an observable to a promise by subscribing to the observable, waiting for it to complete, and resolving the returned promise with the last value from the observed stream.

This replaced the .toPromise() method. I was always a fan of converting my httpClient subscription's to promises, since your not really subscribing to a RESTful endpoint.

You can even throw a .catch() after, since lastValueFrom returns a promise! That is also the reason you get to use await ;)

Hopefully this helps!

import { lastValueFrom } from 'rxjs';

async ngOnInit(){

  this.activeCities = await lastValueFrom(this.fbService.getActiveCities()).catch(err => {});
  
  for (let city of this.activeCities) {
    var data = await lastValueFrom(this.fbService.getFamiliesForCity(city.id));
     let families: Family[] = [];
     families = data;
     this.activeFamilies.set(city .id, families);
  }
  
}

Docs: https://rxjs.dev/api/index/function/lastValueFrom

CodePudding user response:

Easiest solution is to just call your second function inside your first subscription:


  ngOnInit() {
    this.getActiveCities();
  }

  getActiveCities() {
    this.fbService.getActiveCities().subscribe(
      data => {
        this.activeCities = data;
        this.getAllFamilies();
      },
      error => {
        console.log("Error retrieving active cities");
      }
    );
  }

Although a better design is to keep everything as observables and subscribe with the async pipe in html.

export class HomeComponent implements OnInit {
  constructor(private fbService: FirebaseService) { }

  activeFamiliesMap = new Map<string, Observable<Family[]>>();

  activeCities$: Observable<any[]> = this.fbService.getActiveCities().pipe(
    tap((activeCities) => {
      for (const city of activeCities) {
        this.activeFamiliesMap.set(city.id, this.activeFamilies(city.id));
      }
    }),
    catchError((err) => {
      console.error('Error retrieving active cities', err);
      return [];
    })
  );

  activeFamilies(id: any): Observable<Family[]> {
    return this.fbService.getFamiliesForCity(id).pipe(
      catchError((err) => {
        console.error('Error retrieving families for city id:', id, err);
        return [];
      })
    );
  }
}

Just an example of how to display the data:

<div>Active Cities</div>
<pre>{{ activeCities$ | async | json }}</pre>

<ng-container *ngFor="let city of activeCities$ | async">
  <div>City Id: {{ city.id }}</div>
  <div>Families:</div>
  <pre>{{ activeFamiliesMap.get(city.id) | async | json }}</pre>
</ng-container>

stackblitz: https://stackblitz.com/edit/angular-ivy-trkuqx?file=src/app/app.component.ts

  • Related