35

What would be the most idiomatic way to yield values of an Observable by a specific amount of time? For example, let's say I have an Observable created from a big Array and I want to yield a value every 2 seconds. Is a combination of interval and selectMany the best way?

Sergi Mansilla
  • 12,495
  • 10
  • 39
  • 48

11 Answers11

37

For your specific example, the idea is to map each value from the array to an observable that will yield its result after a delay, then concatenate the resulting stream of observables:

var delayedStream = Rx.Observable
    .fromArray([1, 2, 3, 4, 5])
    .map(function (value) { return Rx.Observable.return(value).delay(2000); })
    .concatAll();

Other examples might indeed make use of timer or interval. It just depends.

For example, if your array is really really big, then the above will cause a fair amount of memory pressure (because it is creating N observables for a really large N). Here is an alternative that uses interval to lazily walk the array:

var delayedStream = Rx.Observable
    .interval(2000)
    .take(reallyBigArray.length) // end the observable after it pulses N times
    .map(function (i) { return reallyBigArray[i]; });

This one will yield the next value from the array every 2 seconds until it has iterated over the entire array.

Brandon
  • 38,310
  • 8
  • 82
  • 87
26

I think that using zip produce better and more readable code, still using just 3 observables.

var items = ['A', 'B', 'C'];

Rx.Observable.zip(
  Rx.Observable.fromArray(items),
  Rx.Observable.timer(2000, 2000),  
  function(item, i) { return item;}
)
farincz
  • 4,943
  • 1
  • 28
  • 38
  • maybe a ```take(items.length)``` operator should be added to the timer to allow the obervable to ever complete – Charles HETIER Oct 19 '18 at 19:57
  • `take(items.length)` is completely useless. Stream itself is immutable object, and take (or any other operation) is not affecting source observable but derives a new object from it. Which means your proposal creates just 4 observabbles instead of 3. Completion of `zip` is managed by source array and all memory cleanup is then up to RxJS – farincz Oct 23 '18 at 12:25
  • 2
    Careful - I think this may not work in most practical cases / where the first observable (items) pushes items at unpredictable times. Imagine eg items is delayed by 10 seconds. By the time it produces its items, timer has already ticked 5 times, so all items would be immediately pushed out, and not spaced out by 2 seconds each – Bogey Oct 04 '19 at 15:31
  • @Bogey you are right, this only separate arrays or 'fast' observables – farincz Oct 05 '19 at 22:19
11

For RxJS v6 getting the next one with a delay of 2 seconds.

Example 1. concatMap:

import {of} from 'rxjs';
import {concatMap, delay} from 'rxjs/operators';

of(1, 2, 3, 4, 5)
  .pipe(
    concatMap(x => of(x)
      .pipe(
        delay(2000))
    )
  )
  .subscribe({
    next(value) {
      console.log(value);
    }
  });

Example 2. map + concatAll:

import {of} from 'rxjs';
import {concatAll, delay, map} from 'rxjs/operators';

of(1, 2, 3, 4, 5)
  .pipe(
    map(x => of(x)
      .pipe(
        delay(2000))
    ),
    concatAll()
  )
  .subscribe({
    next(value) {
      console.log(value);
    }
  });
ZoomAll
  • 423
  • 1
  • 6
  • 10
9

While Brandon's answer gets the gist of the idea, here's a version which yields the first item immediately, then puts time between the following items.

var delay = Rx.Observable.empty().delay(2000);

var items = Rx.Observable.fromArray([1,2,3,4,5])
  .map(function (x) {
    return Rx.Observable.return(x).concat(delay); // put some time after the item
  })
  .concatAll();

Updated for newer RxJS:

var delay = Rx.Observable.empty().delay(2000);

var items = Rx.Observable.fromArray([1,2,3,4,5])
  .concatMap(function (x) {
    return Rx.Observable.of(x).concat(delay); // put some time after the item
  });
Simon_Weaver
  • 140,023
  • 84
  • 646
  • 689
cwharris
  • 17,835
  • 4
  • 44
  • 64
  • Note `return` is now `of` : https://github.com/ReactiveX/rxjs/blob/master/MIGRATION.md – Simon_Weaver Jan 12 '18 at 23:51
  • This can now be `concatMap` (guessing concatMap is newer than 2014?). This just queues them all together (otherwise they are all emitted together with a delay following) – Simon_Weaver Jan 12 '18 at 23:59
  • eg. `.concatMap(x => Observable.of(x).concat(Observable.empty().delay(5000)))` – Simon_Weaver Jan 12 '18 at 23:59
  • 1
    Simon, feel free to update the answer with the new naming conventions and simplifications. – cwharris Jan 13 '18 at 00:00
  • 2
    Thx. This was the best solution for me. I want to show notifications (like toast notifications) instantly, but delay the 2nd notification if two arrive at once :-) – Simon_Weaver Jan 13 '18 at 00:06
6

For RxJS 5:

Rx.Observable.from([1, 2, 3, 4, 5])
  .zip(Rx.Observable.timer(0, 2000), x => x)
  .subscribe(x => console.log(x));
user3587412
  • 1,053
  • 1
  • 12
  • 13
4

Agree that zip is a clean approach. Here is a reusable function to generate an interval stream for an array:

function yieldByInterval(items, time) {
  return Rx.Observable.from(items).zip(
    Rx.Observable.interval(time),
    function(item, index) { return item; }
  );
}

// test
yieldByInterval(['A', 'B', 'C'], 2000)
  .subscribe(console.log.bind(console));

This builds on farincz's answer, but is slightly shorter by using .zip as an instance method.

Also, I used Rx.Observable.from() because Rx.Observable.fromArray() is deprecated.

Community
  • 1
  • 1
Robert Penner
  • 6,148
  • 1
  • 17
  • 18
4

Since this wasn't mentioned, I think concatMap combined with delay is pretty readable.

Rx.Observable.fromArray([1, 2, 3, 4, 5])
    .concatMap(x => Rx.Observable.of(x).delay(1000));

See https://codepen.io/jmendes/pen/EwaPzw

Ruan Mendes
  • 90,375
  • 31
  • 153
  • 217
0

Building on the zip solutions by farincz and user3587412, here is how it works in RxJS v6

const { zip, from, timer } = require("rxjs")
const { map } = require("rxjs/operators")

const input = [1, 2, 3, 4, 5]
const delay = 2000

zip(
    from(input),
    timer(0, delay)
).pipe(
    map(([ delayedInput, _timer ]) => delayedInput) // throw away timer index
).subscribe(
    console.log
)
0
//create a custom operator
const delayEach=(millis)=>(o)=>o.pipe(concatMap((x)=>of(x).pipe(delay(millis))))



of(1, 2, 3, 4, 5)
  .pipe(delayEach(1000))
  .subscribe(console.log);
Jannis Ioannou
  • 1,692
  • 1
  • 14
  • 17
0

RxJs 6 code that emits the first item immediately and delays the remaining items:

import { of, EMPTY, concat } from "rxjs";
import { concatMap, delay } from "rxjs/operators";

const delayed$ = EMPTY.pipe(delay(1000));

console.log("start");
of(1, 2, 3, 4)
  .pipe(concatMap(v => concat(of(v), delayed$)))
  .subscribe({
    next: console.log
  });

Full Stackblitz example

idea:

  • for each item we create an observable (using concat) that will output the item immediately (of(v)) and then emit an EMPTY observable after the delay
  • since we use concatMap all emitted observables will be emitted in the correct order
TmTron
  • 17,012
  • 10
  • 94
  • 142
0

A simple one-liner:

const delayMs = 2000
from([1, 2, 3]).pipe(concatMap(x => of(x).pipe(delay(delayMs)))).subscribe(item => {

});
Emmanuel
  • 4,933
  • 5
  • 46
  • 71