For more tips like this, sign up to the weekly newsletter!

RxJS reduce vs scan

RxJS provides two similar functions to build up a value from a stream of events. The first is reduce, which is similar to the Array.reduce function. The second is scan.

Both use an accumulator function that gets the partial result and the current element and returns the next result.

The main difference is that reduce emits only the final result on completion, while scan emits the partial result on each element.

To demonstrate the difference, consider the following examples:

const events = new rxjs.Subject();

events.pipe(
  rxjs.operators.reduce((a, e) => a + e),
).subscribe(console.log.bind(console));<Paste>

events.next(1);
events.next(2);
events.complete(); // 3

And for scan:

const events = new rxjs.Subject();

events.pipe(
  rxjs.operators.scan((a, e) => a + e),
  ).subscribe(console.log.bind(console));
)

events.next(1); // 1
events.next(2); // 3
events.complete();

Use scan if you need a value for every element, and use reduce if you are interested only in the final result.

Convert scan to reduce

At first sight, scan seems easily convertible to reduce; at the time of writing, even the official documentation suggested that only a last() is needed.

Most of the time, this holds:

const events = new rxjs.Subject();

events.pipe(
  rxjs.operators.scan((a, e) => a + e),
  rxjs.operators.last(),
).subscribe(console.log.bind(console));

events.next(1);
events.next(2);
events.complete(); // 3

But for empty observables, neither scan nor reduce produce a value and last() throws an Exception:

const events = new rxjs.Subject();

events.pipe(
  rxjs.operators.scan((a, e) => a + e),
  rxjs.operators.last(),
  ).subscribe(
    console.log.bind(console),
    (e) => console.log("Error") // Error
  );

events.complete();

This edge case can be remedied by using takeLast(1) instead of last():

const events = new rxjs.Subject();

events.pipe(
  rxjs.operators.scan((a, e) => a + e),
  rxjs.operators.takeLast(1),
).subscribe(console.log.bind(console));

events.complete();

But after all this, they still behave differently when a seed is specified. reduce returns the seed for an empty observable:

const events = new rxjs.Subject();

events.pipe(
  rxjs.operators.reduce((a, e) => a + e, 0),
).subscribe(console.log.bind(console));

events.complete(); // 0

While scan does not:

const events = new rxjs.Subject();

events.pipe(
  rxjs.operators.scan((a, e) => a + e, 0),
).subscribe(console.log.bind(console));

events.complete();

To accurately emulate reduce, move the seed to a startWith:

const events = new rxjs.Subject();

events.pipe(
  rxjs.operators.startWith(0),
  rxjs.operators.scan((a, e) => a + e),
  rxjs.operators.takeLast(1),
).subscribe(console.log.bind(console));

events.complete(); // 0
Try it
References
Learn more: