The RxJS groupBy operator uses a key selector to group values into Observables. All values from the source Observable having the same key calculated by the key function land in the same GroupedObservable. A GroupedObservable emits the values of a single group.
Use groupBy to create groups with the modulo operator
One of the easiest example to group numbers is probably when the modulo operator is applied. In the following code we’re using the modulo operator in the key function to create a group for even and odd values each:
const result$: Observable<GroupedObservable<boolean, number>> = of(1,2,3,4,5,6).pipe(
groupBy(v => v % 2 === 0),
);
result$.forEach(group$ => {
group$.subscribe({
next: v => console.log(`Group for key ${group$.key} emitted ${v}`)
})
});
The return of groupBy is a nested Observable, i.e. Observable<GroupedObservable<boolean, number>>
. We cannot directly subscribe to that. But GroupedObservable is a key value structure and therefore we can loop through it with forEach. This gives us each Observable for a single group to which we can subscribe. And conveniently we also have access to the key of the group. The final output is therefore:
Group for key false emitted 1
Group for key true emitted 2
Group for key false emitted 3
Group for key true emitted 4
Group for key false emitted 5
Group for key true emitted 6
Use mergeMap to flatten the grouped Observables
Normally you want to remain reactive when using groupBy and not break the stream with a forEach like above. This is where a flattening operator like mergeMap comes into play. With mergeMap we can join the groups again. But by considering the group’s key during mapping we still have the group:
const result$ = of(1,2,3,4,5,6).pipe(
groupBy(v => v % 2 === 0),
mergeMap(group => group.pipe(toArray()))
);
result$.subscribe(console.log);
For the mapping in mergeMap we just use the utility operator toArray that returns us an array for each group. Finally, the output is:
[ 1, 3, 5 ]
[ 2, 4, 6 ]
Exercise for the RxJS groupBy operator 💪
Divide the users below into age groups. The age groups should be as follows: below 18 (YOUTH), from 19-64 (ADULT), and 65+ (SENIOR).
const users = [
{ name: 'Sue', age: 17 },
{ name: 'Joe', age: 30 },
{ name: 'Frank', age: 25 },
{ name: 'Steve', age: 12 },
{ name: 'Sarah', age: 35 },
{ name: 'Tom', age: 50 },
{ name: 'Catherine', age: 65 },
];
The final output should be in the form of usersGroupedByAge
:
type ageGroup = 'YOUTH' | 'ADULT' | 'SENIORS';
interface User {
name: string;
age: number;
}
type usersGroupedByAge = { [key in ageGroup]?: User[] };
Reduce could help:
reduce((acc, curr: User) => {
if (!acc[group.key]) {
acc[group.key] = [];
}
acc[group.key].push(curr);
return acc;
}, {})
As always the code examples and exercise solution can be found on GitHub.