61

My backend frequently returns data as an array inside an RxJS 5 Observable (I'm using Angular 2).

I often find myself wanting to process the array items individually with RxJS operators and I do so with the following code (JSBin):

const dataFromBackend = Rx.Observable.of([
  { name: 'item1', active: true },
  { name: 'item2', active: false },
  { name: 'item3', active: true }
]);

dataFromBackend
  // At this point, the obs emits a SINGLE array of items
  .do(items => console.log(items))
  // I flatten the array so that the obs emits each item INDIVIDUALLY
  .mergeMap(val => val)
  // At this point, the obs emits each item individually
  .do(item => console.log(item))
  // I can keep transforming each item using RxJS operators.
  // Most likely, I will project the item into another obs with mergeMap()
  .map(item => item.name)
  // When I'm done transforming the items, I gather them in a single array again
  .toArray()
  .subscribe();

The mergeMap(val => val) line doesn't feel very idiomatic.

Is there a better way to apply transformations to the members of an array that's emitted by an Observable?

NB. I want RxJS operators (vs array methods) to transform my items because I need the ability to project each item into a second observable. Typical use case: backend returns of list of item ids and I need to request all of these items from the backend.

martin
  • 93,354
  • 25
  • 191
  • 226
AngularChef
  • 13,797
  • 8
  • 53
  • 69
  • I don't know why atm. But calling any flatAll operator like mergeAll or concatAll (doesn't matter since synchronous anyways). Will return as observables each value. – Harry Scheuerle Jul 17 '20 at 15:06

6 Answers6

66

You can use concatAll() or mergeAll() without any parameter.

dataFromBackend.pipe(
  tap(items => console.log(items)),
  mergeAll(), // or concatAll()
)

This (including mergeMap) works only in RxJS 5+ because it treats Observables, arrays, array-like objects, Promises, etc. the same way.

Eventually you could do also:

mergeMap(val => from(val).pipe(
  tap(item => console.log(item)),
  map(item => item.name),
)),
toArray(),

Jan 2019: Updated for RxJS 6

martin
  • 93,354
  • 25
  • 191
  • 226
  • 1
    Thanks, Martin. Is it correct that `concatAll()` would preserve the order of the items in the original array and `mergeAll()` would not? – AngularChef Feb 27 '17 at 10:17
  • 3
    @AngularFrance Since you're unpacking an array then it doesn't matter which one you use and both will produce ordered items. This is relevant only when used with asynchronous operation which is not this case. – martin Feb 27 '17 at 10:23
  • 1
    What you need is just `map(vals => vals.map(item => item.name)`. No extra `mergeMap, from, toArray` and etc. Should you be mapping each value to the Observable, then it's a different story: https://stackblitz.com/edit/rxjs-xzocbh?file=index.ts – Alex Okrushko Apr 18 '19 at 14:27
5

Actually if you need it inside the stream just use this:

.flatMap(items => of(...items))
Nick Waits
  • 104
  • 1
  • 2
  • There's no effective difference between this and just `items => items`. You could maybe argue this is more 'readable' in a sense but then again if you didn't know what `flatMap()` did then neither would make any sense. They both produce `Observable` where T is whatever the array type is. – Simon_Weaver Feb 22 '19 at 18:58
  • 2
    Just a note that now it's called `mergeMap`, not `flatMap` – Kirill Groshkov Aug 20 '19 at 22:19
5

Angular 6 note.

If you are using as a pipeable operator, do is known as tap!

https://www.learnrxjs.io/operators/utility/do.html Here is an example.

// RxJS 
import { tap, map, of, mergeMap} from 'rxjs/operators';

backendSource
  .pipe(
   tap(items => console.log(items)),
   mergeMap(item => item ),
   map(item => console.log(item.property))
  );
englishPete
  • 809
  • 10
  • 15
3

If it is a synchronous operation, I would suggest to use javascript's Array.map instead, it even should save you some performance:

const dataFromBackend = Rx.Observable.of([
  { name: 'item1', active: true },
  { name: 'item2', active: false },
  { name: 'item3', active: true }
]);

dataFromBackend
  .map(items => items.map(item => item.name))
  .subscribe();
olsn
  • 16,644
  • 6
  • 59
  • 65
  • That's the thing, more often than not I find myself projecting each item into a second observable. Typical use case: backend returns of list of item ids and I need to request all of these items from backend. I should have included that in my question. – AngularChef Feb 27 '17 at 10:08
  • 1
    This does make a difference regarding to how to build your stream - but in that case you are probably better of with @martin's answer - though I have to make a personal (slightly unrelated) comment here: It is generally a bad REST-architecture to first request a list of ids and then fetch their details separately in single rest-calls - that's just torture for any server and usually takes way longer than simply retrieving all data in a single first call. – olsn Feb 27 '17 at 10:32
  • 2
    Sure. But as you know sometimes we are the mere *consumers* of the API, not its *creators*. Just recently I faced the use case I just described (having to make separate calls for the list of keys, then for the entities themselves) with the Gmail JavaScript API. – AngularChef Feb 27 '17 at 10:35
0

I had a similar problem:

The following

  • runs on each item,
  • plucks a deeply nested object
  • converts the server response to back an array
  • consumed in template as async pipe
    this.metaData = backendPostReq(body)
      .pipe(
        mergeMap(item => item),   // splits the array into individual objects
        pluck('metaData', 'dataPie', 'data'), // plucks deeply nested property
        toArray()  // converted back to an array for ag grid
      )

    <ag-grid-angular
        *ngIf="metaData | async as result"
        [gridOptions]="dataCollectionConfig"
        [rowData]="result"
        [modules]="modules"
        class="ag-theme-balham data-collection-grid"
    >
    </ag-grid-angular>

pixlboy
  • 1,452
  • 13
  • 30
0

The most idiomatic is probably .concatMap(arr => rxjs.from(arr))

Sarsaparilla
  • 6,300
  • 1
  • 32
  • 21