I'm trying to convert my Observable of an Object with DocumentReference
s to an Observable of my entire Object.
My Firestore query returns an Observable of QuestDocument
, which looks as follows (stripped of primitive types):
export interface QuestDocument {
...
owner: DocumentReference<User>;
...
collaborators?: DocumentReference<User>[];
categories?: DocumentReference<Category>[];
}
Within my converter, I can call my other Firestore services to retrieve the values of the DocumentReference
s to User
and Category
(flat structures, so no problems here).
My goal is to create an Observable of type Quest
, but my nested Observables are not being resolved correctly.
export interface Quest {
...
owner: User;
...
collaborators?: User[];
categories?: Category[];
}
Here's what I have so far:
doc$(docId: string): Observable<Quest> {
return this.doc(docId).valueChanges()
.pipe(
mergeMap(questDoc => {
const owner$ = this.userService.doc$(questDoc.owner.id);
const collaborators$ = forkJoin(questDoc.collaborators.map(
(userRef: DocumentReference) => {
return this.userService.doc$(userRef.id)
}
));
const categories$ = forkJoin(questDoc.categories.map(
(categoryRef: DocumentReference) => this.categoryService.doc$(categoryRef.id)
));
const joined = forkJoin({
owner: owner$,
collaborators: collaborators$,
categories: categories$
});
joined.subscribe(data => console.log(data));
return joined.pipe(
map(value => {
return Object.defineProperties(questDoc, {
qid: { value: docId },
owner: { value: value.owner },
collaborators: { value: value.collaborators },
categories: { value: value.categories }
}) as Quest;
})
)
})
);
}
All the typings match up and I should receive an Observable<Quest>
, but when I try to print the value it returns undefined
and the second .pipe()
is never reached.
You are not using the forkJoin
operator correctly.
An observable that is complete will no longer emits data. Take it like a closed pipe.
ForkJoin
will wait that all steams are completed (closed) before emitting one single data.
If you're fetching your data using this.afs.collection(...).doc(...).valueChanges()
, these observables stay active. They will emit each time the data is updated in firestore.
To complete them, use a take(1)
or first()
(they will emit once then complete), or use combineLatest()
to combine active streams and keep your data updated in real time (don't forget to unsubscribe onDestroy to prevent any memory leak).
Here's an example with completed streams:
doc$(docId: string): Observable<Quest> {
return this.doc(docId).valueChanges()
.pipe(
mergeMap(questDoc => {
// observable will emit then complete thanks to the "take(1)"
const owner$ = this.userService.doc$(questDoc.owner.id).pipe(take(1));
const collaborators$ = forkJoin(questDoc.collaborators.map(
(userRef: DocumentReference) => {
// same thing here
return this.userService.doc$(userRef.id).pipe(take(1))
}
));
const categories$ = forkJoin(questDoc.categories.map(
// and here
(categoryRef: DocumentReference) => this.categoryService.doc$(categoryRef.id).pipe(take(1))
));
const joined = forkJoin({
owner: owner$,
collaborators: collaborators$,
categories: categories$
});
// joined.subscribe(data => console.log(data));
return joined.pipe(
// NEVER subscribe within a pipe, use a tap operator for side effects
tap(data => console.log(data)),
map(value => {
return Object.defineProperties(questDoc, {
qid: { value: docId },
owner: { value: value.owner },
collaborators: { value: value.collaborators },
categories: { value: value.categories }
}) as Quest;
})
)
})
);
}