I have prepared this example on Stackblitz for my question. In this Angular app, I have a NestedSolutionComponent
with my current working solution and an AppComponent
, where I want to achieve the same result by using proper rxjs operations.
For a real world example which is a bit more complex, I am looking for a solution to map the results of my multiple inner subscription to an array of my outer subscription.
A REST call for a user service provides me this array:
[
{
id: 1,
name: 'User One',
groupIds: [1, 3]
},
{
id: 2,
name: 'User Two',
groupIds: [2, 3, 4]
},
]
And for each group, I would like to call a REST group service that is providing me further infos about the user group. All in all, I call the group service 5 times, each ID in the group array gets called once. When finished, the result should be mapped into the groups array - but instead of solely having the ID, the whole object should be stored into the array.
The solution should look like this:
[
{
id: 1
name: 'User One'
groups: [
{ id: 1, name: 'Group One' },
{ id: 3, name: 'Group Three' }
]
},
{
id: 2
name: 'User Two'
groups: [
{ id: 2, name: 'Group Two' },
{ id: 3, name: 'Group Three' },
{ id: 4, name: 'Group Four' }
]
}
]
By nesting subscription, the solution is easy - but ugly. I call the user service first, then calling for each user each groups:
this.appService.getUsers().subscribe((users) => {
const _usersWithGroupNames = users.map((user) => {
const userWithGroupNames = {
id: user.id,
name: user.name,
groups: [],
} as UserWithGroupNames;
user.groupIds.forEach((groupId) => {
this.appService.getGroupById(groupId).subscribe((groupWithName) => {
userWithGroupNames.groups.push({
id: groupWithName.id,
name: groupWithName.name,
});
});
});
return userWithGroupNames;
});
this.usersWithGroupNames.next(_usersWithGroupNames); // Subject
});
I have spent hours and hours but I really do not see any solution with proper rxjs operators. I tried switchMap
and mergeMap
but ended in a hell of nested map operations. Also forkJoin
seems not to help me out here, since I receive an array and I have to call the inner subscriptions in a certain order. When I call multiple mergeMaps in a pipe, I cannot access the previous values. I would like to have a solution like that
// not real code, just dummy code
userService.pipe(
xmap(users => generateUsersWithEmptyGroupArray()),
ymap(users => users.groups.forEach(group => groupService.getGroup(group)),
zmap((user, groups) => mapUserWithGroups(user, groups)) // get single user with all group information
).subscribe(usersWithGroups => this.subject.next(usersWithGroups))
Anybody here who knows a proper and readable solution for my problem?
Thanks a lot in advance!
CodePudding user response:
Not sure what you've tried, but this is how I would structure this stream. It's a switchMap
(Which could be a mergeMap
or concatMap
instead, it shouldn't matter in this case), a forkJoin
(Not sure why it didn't work for you, but it should from what I've seen), and a map
to create the final user with group names.
If you have any questions, I'll be happy to update this answer with some clarifications.
interface User {
id: number,
name: string,
groupIds: number[]
}
interface UserWithGroupNames {
id: number,
name: string,
groups: any[]
}
class ArbiratryClassName {
public usersWithGroupNames$: Observable<UserWithGroupNames[]>;
embellishUser(user: User): Observable<UserWithGroupNames> {
// forkJoin to get all group names
return forkJoin(
user.groupIds.map(groupId =>
this.appService.getGroupById(groupId)
)
).pipe(
// map to create final UserWithGroupNames
map(groups => ({
id: user.id,
name: user.name,
groups
}) as UserWithGroupNames)
);
}
arbiratryInit(): void {
// instead of this.usersWithGroupNames Subject, create
// the stream directly as usersWithGroupNames$, and subscribe to
// usersWithGroupNames$ whereever you'd use the subject.
this.usersWithGroupNames$ = this.appService.getUsers().pipe(
switchMap((users: User[]) =>
forkJoin(users.map(u => this.embellishUser(u)))
)
);
}
}
CodePudding user response:
First approach : Used switchMap, mergeMap, from, forkJoin
this.appService
.getUsers()
.pipe(
switchMap((users) =>
// for each user
from(users).pipe(
// merge map to run parallel for each user
mergeMap(({ groupIds, ...user }) =>
// wait to retrive all group details of current user at mergeMap
// after completing use map to map user with retrived group
forkJoin(
groupIds.map((id) => this.appService.getGroupById(id))
).pipe(map((groups) => ({ ...user, groups })))
)
)
)
)
.subscribe((result) => {
console.log(result);
});
In the above code, forkJoin
will wait to get all groupIds
details for a particular user, and also if he has retrieved group id 3 for the first user it will again retrieve groupId
3 details for user 2 and so on. In short duplicate group, details will be retrieved.
Second approach : Below is the approach we will get all groupsIds
out of the user array, make them unique, get all details of them in parallel, and at the end, we will map group details to users by their groupIds
, Here we will not wait for each user group id details to retrieve and also duplicate group details will not be retrieved.
this.appService
.getUsers()
.pipe(
switchMap((users) =>
// get all unique groupIds of all users
from(this.extractUniqueGroupIds(users)).pipe(
// parallell fetch all group details
mergeMap((groupId) => this.appService.getGroupById(groupId)),
// wait to to complete all requests and generate array out of it
reduce((acc, val) => [...acc, val], []),
// to check retrived group details
// tap((groups) => console.log('groups retrived: ', groups)),
// map retrived group details back to users
map((groups) => this.mapGroupToUsers(users, groups))
)
)
)
.subscribe((result) => {
console.log(result);
// this.usersWithGroupNames.next(result);
});
private mapGroupToUsers(users: User[], groups: Group[]): UserWithGroup[] {
return users.map(({ groupIds, ...user }) => ({
...user,
groups: groupIds.map((id) => groups.find((g) => g.id === id)),
}));
}
private extractUniqueGroupIds(users: User[]): number[] {
const set = users.reduce((acc, { groupIds }) => {
groupIds.forEach((id) => acc.add(id));
return acc;
}, new Set<number>());
return [...set];
}
interface UserWithGroup {
id: number;
name: string;
groups: any[];
}