RxJS: Mapping object keys to observables - javascript

I have an app where questions are shown to users.
Drafts for the questions are loaded from a SharePoint list. Each draft contains a key which is used to load proper responses to the question from another SharePoint list. Here's how I currently implemented it:
interface QuestionDraft {
title: string;
responseKey: string;
}
interface Question {
title: string;
responses: string[];
}
const drafts: QuestionDraft[] = [];
const questions: Question[] = [];
// stub
private getDrafts(): Observable<QuestionDraft> {
return from(drafts);
}
// stub
private getResponses(key: string): Observable<string> {
return of(key, key, key);
}
main(): void {
getDrafts().subscribe(
data => {
const res: string[] = [];
getResponses(data.responseKey).subscribe(
d => res.push(d),
error => console.error(error),
() => questions.push({
title: data.title,
responses: res
})
);
}, error => console.error(error),
() => console.log(questions)
);
}
This solution works fine, but I think the code in main() looks messy. Is there an easier way to do the same thing, for example using mergeMap or something similar?

You can use mergeMap to map to a new Observable and toArray to collect the emitted values in an array. Use catchError to handle errors in your streams and map to an alternative Observable on errors.
This code will work just like your code with the emitted questions array containing all questions up until getDrafts throws an error and exluding questions for which getResponses threw an error.
getDrafts().pipe(
mergeMap(draft => getResponses(draft.responseKey).pipe(
toArray(),
map(responses => ({ title: draft.title, responses } as Question)),
catchError(error => { console.error(error); return EMPTY; })
)),
catchError(error => { console.error(error); return EMPTY; }),
toArray()
).subscribe(qs => { console.log(qs); questions = qs; })
Keep in mind that the questions in the final array will not necessarily be in the same order as the drafts coming in. The order depends on how fast a getResponses Observable completes for a specific draft. (This is the same behaviour as your current code)
To ensure that the questions will be in the same order as the drafts you can use concatMap instead of mergeMap. But this might slow down the overall execution of the task as the responses for the next draft will only be fetched after the responses for the previous draft completed.

You can try and use flatMap to make it cleaner.
RxJs Observables nested subscriptions?
this.getDrafts()
.flatMap(function(x){return functionReturningObservableOrPromise(x)})
.flatMap(...ad infinitum)
.subscribe(...final processing)

If you use RxJS version 6 you must use pipe() method and turned flatMap into mergeMap.
in rxjs 6, example of #emcee22 will be look:
this.getDrafts()
.pipe(
.mergeMap(function(x){return functionReturningObservableOrPromise(x)}),
.mergeMap(...ad infinitum)
).subscribe(...final processing)

Related

How to ensure observables from different components are complete?

I have a number of components on a page, which all use observables to get API data. I pass these observables to a loading service, which I want to display a loader from when the first observable is passed until the last one has finalised.
Loading service:
private _loading = new BehaviorSubject<boolean>(false);
readonly loading$ = this._loading.asObservable();
showUntilLoadingComplete<T>(observable$: Observable<T>): Observable<T> {
return of(null).pipe(
tap(_ => this._loading.next(true)),
concatMap(_ => observable$),
finalize(() => this._loading.next(false))
);
}
My components then call loading service like so:
this.loadingService.showUntilLoadingComplete(someObservable$)
.subscribe(data=> {
// do stuff
});
However, due to the first observable finalising, the behaviour subject gets passed false and this in turn stops the loader from showing. I have considered creating another behaviour subject to store an array of the active observables, and remove them from here once finalised, and then subscribing to that and setting the loader off once the array has no length. But this doesn't seem like a great solution, so I am looking for others input.
Since you're depending on the same loading$ Observable in a singleton service, then you can add another property to reflect the active number of calls, then turn the loading off only if there is no other active call.
Try something like the following:
private _active: number = 0;
private _loading = new BehaviorSubject<boolean>(false);
readonly loading$ = this._loading.asObservable();
showUntilLoadingComplete<T>(observable$: Observable<T>): Observable<T> {
return of(null).pipe(
tap(() => {
this._loading.next(true);
this._active++;
}),
concatMap((_) => observable$),
finalize(() => {
this._active--;
if (!this._active) {
this._loading.next(false);
}
})
);
}

Iterate through Observable object-array and add additional data from another Observable to every object

I have a Angular service returning an array of "cleaningDuty" objects. Inside a duty object, there is a nested object called "currentCleaner" with an id.
[
{
// other cleaningDuty data
currentCleaner: {
id: string;
active: boolean;
};
},
{
// other cleaningDuty data
currentCleaner: {
id: string;
active: boolean;
};
},
{
// other cleaningDuty data
currentCleaner: {
id: string;
active: boolean;
};
}
]
with the help of the currentCleaner.id I want to fetch the user data of the currentCleaner from a UserService dynamically in the same pipe() method and add the returned user data to the cleaningDuty object. Then the object should look like this:
{
// other cleaningDuty data
currentCleaner: {
id: string;
active: boolean;
},
cleanerData: {
name: string;
photoUrl: string;
// other user data
}
},
Unfortunately I just cant get this to work even after investing days into it. I tried almost every combination from forkJoin(), mergeMap() and so on. I know a nested subscribe() method inside the target component would get the job done, but I want to write the best code possible quality-wise. This is my current state of the service method (it adds the user observable instead of the value to the cleaningDuty object):
getAllForRoommates(flatId: string, userId: string) {
return this.firestore
.collection('flats')
.doc(flatId)
.collection('cleaningDuties')
.valueChanges()
.pipe(
mergeMap((duties) => {
let currentCleaners = duties.map((duty) =>
this.userService.getPublicUserById(duty.currentCleaner.id),
);
return forkJoin([currentCleaners]).pipe(
map((users) => {
console.log(users);
duties.forEach((duty, i) => {
console.log(duty);
duty.cleanerInfos = users[i];
});
return duties;
}),
);
}),
);
}
The getPublicUserById() method:
getPublicUserById(id: string) {
return this.firestore.collection('publicUsers').doc(id).valueChanges();
}
Any help would be greatly appreciated!
The forkJoin function will:
Wait for Observables to complete and then combine last values they emitted; complete immediately if an empty array is passed.
So, you have to make sure that all the inner Observables have been completed, then the forkJoin will emit the combined values, and you can achieve that using take(1) operator, like the following:
getAllForRoommates(flatId: string, userId: string) {
return this.firestore
.collection('flats')
.doc(flatId)
.collection('cleaningDuties')
.valueChanges()
.pipe(
mergeMap((duties) =>
// The `forkJoin` will emit its value only after all inner observables have been completed.
forkJoin(
duties.map((duty) =>
// For each duty, fetch the cleanerData, and append the result to the duty itself:
this.userService.getPublicUserById(duty.currentCleaner.id).pipe(
take(1), // to complete the sub observable after getting the value from it.
map((cleanerData) => ({ ...duty, cleanerData }))
)
)
)
)
);
}
Check if you need switchMap or mergeMap. IMO I'd cancel the existing observable when the valueChanges() emits. See here for the difference b/n them.
Use Array#map with RxJS forkJoin function to trigger the requests in parallel.
Use RxJS map operator with destructuring syntax to add additional properties to existing objects.
Ideally defined types should be used instead of any type.
Try the following
getAllForRoommates(flatId: string, userId: string) {
return this.firestore
.collection('flats')
.doc(flatId)
.collection('cleaningDuties')
.valueChanges()
.pipe(
switchMap((duties: any) =>
forkJoin(
duties.map((duty: any) => // <-- `Array#map` method
this.userService.getPublicUserById(duty.currentCleaner.id).pipe(
map((data: any) => ({ // <-- RxJS `map` operator
...duty, // <-- the `currentCleaner` property
cleanerData: data // <-- new `cleanerData` property
}))
)
)
)
)
);
}
Firstly I would suggest you to improve the typings on your application, that will help you to see the issue more clear.
For my answer I will assume you have 3 interfaces defined CurrentCleaner, CleanerData and RoomData wich is just:
interface RoomData {
currentCleaner: CurrentCleaner
cleanerData: CleanerData
}
Then you can write a helper function to retrieve the room data from a current cleaner:
getRoomData$(currentCleaner: CurrentCleaner): Observable<RoomData> {
return this.userService.getPublicUserById(currentCleaner.id).pipe(
map(cleanerData => ({ currentCleaner, cleanerData }))
)
}
As you can see, this functions take a current cleaner, gets the cleaner data for it and maps it into a RoomData object.
Having this next step is to define your main function
getAllForRoommates(flatId: string, userId: string): Observable<RoomData[]> {
return this.firestore
.collection('flats')
.doc(flatId)
.collection('cleaningDuties')
.valueChanges()
.pipe(
mergeMap((duties) => {
// here we map all duties to observables of roomData
let roomsData: Observable<RoomData>[] = duties.map(duty => this.getRoomData$(duty.currentCleaner));
// and use the forkJoin to convert Observable<RoomData>[] to Observable<RoomData[]>
return forkJoin(roomsData)
}),
);
}
I think this way is easy to understand what the code is doing.

Angular and RxJs combine two http requests

I am making two http requests, each return a observable<IProduct>; and I want to combine both into a local object and use the async pipe to bring values from each.
productA$: observable<IProduct>;
productB$: observable<IProduct>;
combinedProds$: ?
this.productA$ = httpCall();
this.productB$ = httpCall();
this.combinedProds$ = combineLatest([
this.productA$,
this.productB$
])
.pipe(
map(([productA, productB]) =>
({ productA, productB}))
);
This issue I'm getting, I don't know what type combinedProds$ should be.
Maybe forkJoin is the one you are looking for ?
forkJoin work best with Http call and I'm using it a lot when dealing with http request
// RxJS v6.5+
import { ajax } from 'rxjs/ajax';
import { forkJoin } from 'rxjs';
/*
when all observables complete, provide the last
emitted value from each as dictionary
*/
forkJoin(
// as of RxJS 6.5+ we can use a dictionary of sources
{
google: ajax.getJSON('https://api.github.com/users/google'),
microsoft: ajax.getJSON('https://api.github.com/users/microsoft'),
users: ajax.getJSON('https://api.github.com/users')
}
)
// { google: object, microsoft: object, users: array }
.subscribe(console.log);
Update
forkJoin return an Observable<any> so you can change your like this
combinedProds$: Observable<any>

Chaining observables Angular 5 / Rxjs

I am a bit confused with rxjs operators. I have few api calls that return observable:
getCurrentUser(): Observable<any> {
return this.http.get<any>(userUrl);
}
tagsList(): Observable<string[]> {
return this.http.get<string[]>(tagsUrl);
}
timezonesList(): Observable<Timezone[]> {
return this.http.get<Timezone[]>(timezonesUrl);
}
I want to call getCurrentUser() then with result of returning value call action LoadUser(user)
Then after user loads call multiple async requests at the same time:
tagsList(), timezonesList()
And then with results of returning value of them call actions LoadTags(tags), LoadTimezones(timezones)
So it should looks like something like this:
init() {
this.accountsApi.getCurrentUser()
.map((user: User) => this.store.dispatch(new LoadUser({ user })))
.map(
this.commonApi.tagsList(),
this.commonApi.timezonesList(),
this.commonApi.authoriztionServicesList()
)
.map((tags, timezones, authorizationServices) => {
this.store.dispatch(new tagsActions.LoadTags(tags));
this.store.dispatch(new timezonesActions.LoadTimezones(timezones));
this.store.dispatch(new authorizationServicesActions.LoadAuthorizationServices(authorizationServices));
});
}
I know that this operators are wrong. What operators should i use for this? I have already done it with promises, but i am sure that i can do it with rxjs operators in less line of code.
P.S. Also it is interesting for me how i can do this with async / await? Thank you
In your original code you are using map a bit too much, for some use cases you may not need to map.
init() {
return this.accountsApi.getCurrentUser()
.do((user: User) => this.store.dispatch(new LoadUser({ user })))
.forkJoin(
this.commonApi.tagsList(),
this.commonApi.timezonesList(),
this.commonApi.authoriztionServicesList()
)
.do((results) => {
this.store.dispatch(new tagsActions.LoadTags(results[0]));
this.store.dispatch(new timezonesActions.LoadTimezones(results[1]));
this.store.dispatch(new authorizationServicesActions.LoadAuthorizationServices(results[2]));
});
}
forkJoin lets you fire off many observable subscriptions and once all subscriptions produce values you get a single array of observable values back.
The do operator introduces side effects to launch your store actions since you don't want to create any arrays.

How to parallel api calls and keep the order of the responses in a list for the ui to present (RxJS Observables)

Challenge!
My issue is as follow:
I have a function that gets Observable and needs to enrich the person data and update an observer with an Observable
Which Person object looks like:
export interface Person {
personId: string;
children: Child[];
}
export interface Child {
childId: string;
}
and the EnrichPerson looks like:
export interface EnrichedPerson {
personName: string;
parsonCountry: string;
children: EnrichedChild[]
}
export interface EnrichedChild {
childName: string;
childAge: number
}
So, what I did is this:
private myFunc(listOfPeople: Observable<Person[]>): void {
// initializing listOfEnrichedPeople , this will be the final object that will be updated to the behaviour subject
// "public currentListOfPeople = new BehaviorSubject<EnrichedPerson[]>([]);"
let listOfEnrichedPeople: EnrichedPerson[] = [];
listOfPeople.subscribe((people: Person[]) => {
people.map((person: Person, personIdx: number) => {
// here im setting up a new list of enriched children list cause each person have a list like this
// and for each of the children I need to perform also an api call to get its info - youll see soon
let listOfEnrichedChildren: EnrichedChild[] = [];
// here im taking a list of the ids of the people, cause im gonna perform an api call that will give me their names
let ids: string[] = people.map((person: Person) => person.personId);
this._peopleDBApi.getPeopleNames(ids).subscribe((names: string[]) => {
// here I though if I already have the name I can set it up
listOfEnrichedPeople.push({
personName: names[personIdx],
parsonCountry: "",
childrenNames: [] });
// now for each person, i want to take its list of children and enrich their data
person.childrenIds.map((child: Child) => {
// the catch is here, the getChildInfo api only perform it per id and cant recieve a list, and I need to keep the order...so did this in the
this._childrenDBApi.getChildInfo(child.childId).subscribe((childInfo: ChildInfo) => {
listOfEnrichedChildren.push({
childName: childInfo.name,
childAge: childInfo.age});
});
});
listOfEnrichedPeople[personIdx].parsonCountry = person.country;
listOfEnrichedPeople[personIdx].children = listOfEnrichedChildren;
});
});
this.currentListOfPeople.next(listOfEnrichedPeople);
},
error => {
console.log(error);
self.listOfEnrichedPeople.next([]);
});
}
my problem is when I make the children api call, cause I if the first id takes 2 sec to respond and the one after it only 1 sec so im losing my order...i need to keep the order that I originally got in the function parameter...how can I make it parallel for better performance and also keep my orders?
Use the index argument of the .map() callback and assign to the list via that index instead of using .push(). That way, regardless of timing, the api response will get assigned to the correct position in the list.
person.childrenIds.map(({child: Child}, index) => {
this._childrenDBApi.getChildInfo(child.childId).subscribe((childInfo: ChildInfo) => {
listOfEnrichedChildren[index] = {
childName: childInfo.name,
childAge: childInfo.age};
};
// ...
You can generate a new Observable containing the results of getting each child/person from the API (in parallel) and the original index in the array.
You can then flatten all of those results down into a single new array, sort them by the original index and return them
const getEnrichedChildren = (children: Person[]): Observable<EnrichedPerson[]> =>
//create an observable from the array of children
Observable.of(...children)
//map to a new observable containing both the result from the API and the
//original index. use flatMap to merge the API responses
.flatMap((child, index) => peopleApi.getPerson(child.personId).map(enriched => ({ child: enriched, index })))
//combine all results from that observable into a single observable containing
//an array of EnrichedPerson AND original index
.toArray()
//map the result to a sorted list of EnrichedPerson
.map(unorderedChildren => unorderedChildren.sort(c => c.index).map(c => c.child));
The readability of this is pretty terrible here but I've kept everything in one block so you can see how things chain together

Categories