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