dunaskdunsay
dunaskdunsay

Reputation: 151

RXJS groupBy Observable <Object[]>

I have entries$: Observable<BooksOverviewGroup[]>;:

enter image description here

and i would like to group them by groupId. I tried like this:

groupBy(books => books.map(book => book.groupId)),
 mergeMap(innerObs => innerObs.skip(1).map(books => books.map(book => book.groupTitle)));

However, it didn't work. How can I group by groupId in this case ? (Observable<Object[]>)

Upvotes: 15

Views: 14582

Answers (4)

Ingo B&#252;rk
Ingo B&#252;rk

Reputation: 20033

Higher-Order Observable

If you want a higher-order observable, you can actually use the groupBy rxjs operator.

const data = [
  {groupId: "foo", value: 1},
  {groupId: "foo", value: 3},
  {groupId: "foo", value: 5},
  {groupId: "bar", value: 42},
  {groupId: "bar", value: 1337},
];

from(data).pipe(
  groupBy(item => item.groupId)
)
  .subscribe(console.log);

This will result in an observable which emits one observable per group, and on each of those inner observables, the corresponding items will be emitted.

Observable of Groups

If instead you want an observable which simply emits the groups, then this has actually nothing to do with rxjs or its groupBy operator. Instead, it's merely a basic question about how to group an array in Javascript.

There is no built-in method for this, but libraries like lodash come with such functions. You can also do it by hand:

const data = [
  {groupId: "foo", value: 1},
  {groupId: "foo", value: 3},
  {groupId: "foo", value: 5},
  {groupId: "bar", value: 42},
  {groupId: "bar", value: 1337},
];

const groupBy = (data, keyFn) => data.reduce((agg, item) => {
  const group = keyFn(item);
  agg[group] = [...(agg[group] || []), item];
  return agg;
}, {});

of(data).pipe(
  map(data => groupBy(data, item => item.groupId))
)
  .subscribe(console.log);

This will emit once for the entire data set, but will emit

{
  "foo":[
    {"groupId":"foo","value":1},
    {"groupId":"foo","value":3},
    {"groupId":"foo","value":5}
  ],
  "bar":[
    {"groupId":"bar","value":42},
    {"groupId":"bar","value":1337}
  ]
}

Upvotes: 13

Adarsh Johny
Adarsh Johny

Reputation: 11

const data = [
    { groupId: "foo", value: 1 },
    { groupId: "foo", value: 3 },
    { groupId: "foo", value: 5 },
    { groupId: "bar", value: 42 },
    { groupId: "bar", value: 1337 },
];

of(data).pipe(
    mergeMap(res => res),
    groupBy(item => item.groupId),
    mergeMap(obs => {
        return obs.pipe(
            toArray(),
            map(items => {
                return { [obs.key]: items }
            })
        )
    }), toArray()
).subscribe(map((groupResponse) => {
    let groups = {};
    groupResponse.forEach((item) => { groups = { ...groups, ...item }; });
    console.log(groups);
});

Output will be

{
    "foo": [{ "groupId": "foo", "value": 3 }, { "groupId": "foo", "value": 5 }],
    "bar": [{ "groupId": "bar", "value": 42 }, { "groupId": "bar", "value": 1337 }]
} 

Upvotes: 0

Kamil
Kamil

Reputation: 1538

You almost got it.

Try using mergeMap() or concatMap() before groupBy().

So an example:

const people = [
  { name: 'Sue', age: 25 },
  { name: 'Joe', age: 30 },
  { name: 'Frank', age: 25 },
  { name: 'Sarah', age: 35 }
];

of(people) // <- this gets Observable<Object[]>
  .pipe(
    mergeMap(res => res), // <- use concatMap() if you care about the order
    groupBy(person => person.age, p => p.name),
    mergeMap(group => zip(of(group.key), group.pipe(toArray())))
  )
  .subscribe(console.log);

Output:

(2) [25, Array(2)]
(2) [30, Array(1)]
(2) [35, Array(1)]

Upvotes: 18

Luillyfe
Luillyfe

Reputation: 6842

Just use the groupBy operator, then merge them all.

$data.pipe(
    groupBy(book => book.groupId),
    mergeMap(group => group.pipe(toArray()))
)
.subscribe(console.log)

Upvotes: 2

Related Questions