Issue
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`
Solution
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 of([]);
})
);
activeFamilies(id: any): Observable<Family[]> {
return this.fbService.getFamiliesForCity(id).pipe(
catchError((err) => {
console.error('Error retrieving families for city id:', id, err);
return of([]);
})
);
}
}
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
And an even better design may be for your service to return an observable of the map, although it's an ugly beast of an observable. At least it hides the logic from your components:
Service
getFamilyMap(): Observable<Map<string, Family[]>> {
return this.getActiveCities().pipe(
map((activeCities) => {
return activeCities.map((city) => {
return this.getFamiliesForCity(city.id).pipe(
map((families) => {
return { id: city.id, families };
})
);
});
}),
switchMap((data) => forkJoin(data)),
map((data) => {
const res = new Map<string, Family[]>();
for (const entry of data) {
res.set(entry.id, entry.families);
}
return res;
}),
catchError((err) => {
console.error('Error retrieving family map', err);
return of(new Map<string, Family[]>());
})
);
}
Component
export class HomeComponent {
constructor(private fbService: FirebaseService) {}
activeCities$ = this.fbService.getActiveCities();
familyMapEntries$ = this.fbService
.getFamilyMap()
.pipe(map((map) => Array.from(map)));
}
I use Array.from()
rather than map.entries()
because iterators tend to throw changedAfterChecked
errors.
Html
<div>Active Cities</div>
<pre>{{ activeCities$ | async | json }}</pre>
<ng-container *ngFor="let entry of (familyMapEntries$ | async)">
<div>City Id: {{ entry[0] }}</div>
<div>Families:</div>
<pre>{{ entry[1] | json }}</pre>
</ng-container>
stackblitz: https://stackblitz.com/edit/angular-ivy-ffzpya?file=src/app/firebase.service.ts
Answered By - Chris Hamilton
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.