skip to content

In RxJS, what is the difference between scan and reduce, and which would you use for a live cart total?

level: middleimportance: must knowfreq 62%

answer

  1. every step or only the end
  2. completion is the trigger
  3. the seed and the first value
  4. an empty source behaves differently

basics

~20 s

scan emits the updated accumulator after every value; reduce emits only the final accumulator when the source completes. A live cart total needs scan, because the cart stream never completes and reduce would never emit.

solid answer

~40 s

Both take an accumulator `(acc, value, index) => newAcc` and an optional seed. `scan` emits the new accumulator **after each value**, so it is a running state. `reduce` keeps the same state silently and emits **once, when the source completes** - on a stream that never completes it never emits. For a live cart total, `scan((total, item) => total + item.price * item.qty, 0)` updates the total with every addition. Two details matter: without a seed, the first value becomes the accumulator and is emitted as-is by `scan`; and on an empty source `scan` emits nothing, while `reduce` with a seed emits the seed on completion. `toArray()` is the collecting special case of `reduce`, so it also waits for completion.

code

ts · 25 lines
ts
import { Subject, reduce, scan, startWith } from 'rxjs';

interface CartAddition {
  sku: string;
  price: number;
  qty: number;
}

const additions$ = new Subject<CartAddition>();

const liveTotal$ = additions$.pipe(
  scan((total, item) => total + item.price * item.qty, 0),
  startWith(0),
);

const finalTotal$ = additions$.pipe(
  reduce((total, item) => total + item.price * item.qty, 0),
);

liveTotal$.subscribe((t) => console.log('live', t)); // live 0
finalTotal$.subscribe((t) => console.log('final', t));

additions$.next({ sku: 'A', price: 10, qty: 1 }); // live 10
additions$.next({ sku: 'B', price: 5, qty: 2 }); // live 20
additions$.complete(); // final 20

go deeper

for a junior

Recall that scan emits after every value and reduce emits once on completion, so a live total uses scan.

for a middle

Explain the seed rules, why reduce and toArray never emit on endless streams, and why a seeded scan still emits nothing until the first value.

for a senior

Guard state built with scan: return new objects for reference-based change checks, seed when types differ, and add startWith when a UI needs an initial value.

for a principal

Decide when a scan-based local state stream is enough and when accumulated state belongs in a dedicated state layer shared across features.

## Same accumulator, different emission `scan` and `reduce` are RxJS's **accumulation** operators. Both take an accumulator function `(acc, value, index) => newAcc` and an optional **seed**. RxJS 7 implements both on the same internal routine; the only difference is when the accumulated state is emitted. | | `scan(fn, seed?)` | `reduce(fn, seed?)` | |---|---|---| | Emits | after every source value | once, on source completion | | Never-completing source | emits a running value forever | never emits | | Empty source, with seed | emits nothing, then completes | emits the seed, then completes | | Empty source, no seed | emits nothing | emits nothing | | On error | error passes through | error passes through; nothing emitted | ## The cart scenario A shop keeps a stream of cart additions, each with a price and quantity, and the header shows the current total. - `scan` fits: each addition produces a new total immediately. - `reduce` does not: the stream of additions stays open for the whole session, so `reduce` would wait for a completion that never comes and the header would stay blank. - `reduce` is right when the source is finite and only the final figure matters - totalling a completed list of order lines, for example. ## Seeds and the first value The seed decides the **initial accumulator**: 1. **With a seed**, the accumulator runs for every value, starting from the seed: `scan((t, x) => t + x, 0)` over `1, 2, 3` emits `1, 3, 6`. 2. **Without a seed**, the first value becomes the accumulator directly and the function is **not** called for it: `scan((t, x) => t + x)` over `1, 2, 3` emits `1, 3, 6` too - but if the values are objects and the accumulator is a number, the first emission is the raw object, which is a type bug. Always seed when the accumulator's type differs from the value's. 3. **An explicit `undefined` seed counts as a seed.** RxJS checks the number of arguments, not the value. ## The empty-stream gotcha `scan` emits only in response to values. A running total seeded with `0` emits **nothing** until the first addition, so a UI bound to it shows no total at first. Adding `startWith(0)` after `scan` gives an immediate initial value. `reduce` with a seed behaves differently: when the source completes empty, it emits the seed. ## Immutability of the accumulator When the accumulator is an object or array - a cart as a list of lines rather than a number - return a **new** object from each step (`[...lines, item]`, `{ ...cart, count: cart.count + 1 }`) instead of mutating it. Downstream consumers often compare by reference; if `scan` keeps emitting the same mutated object, a check such as `distinctUntilChanged()` or a UI that skips unchanged references will see no change. ## scan as a small state machine `scan` also works with **actions** instead of plain numbers: a stream of `{ type: 'add' }` and `{ type: 'remove' }` objects folded by `scan((state, action) => nextState(state, action), initialState)` produces a stream of states. That reducer shape - state plus action gives the next state - is the idea behind Redux-style stores, which are a separate topic. For one component's local state, a `scan` over a handful of action types is often enough: - the whole state history is derived from the actions, so it is easy to test; - each subscription builds its own state from the seed. ## toArray and friends - **`toArray()`** collects every value into an array and emits it on completion. It is `reduce` with a fresh `[]` seed created per subscription, so it has the same "never emits on an endless stream" property. - **`scan` with an index** - the accumulator's third argument - counts values per subscription, which is handy for numbering. ## Choosing in an interview - "Do I need intermediate results?" Yes: `scan`. Only the final one on a finite stream: `reduce`. - "Does the source complete?" If not, `reduce` and `toArray` will never emit. - Mention the seed rules and the empty-stream difference to show you have used them, not just read the names.

  • In RxJS, what does scan((acc, x) => acc + x) emit first when it has no seed?
    It emits the first source value unchanged. Without a seed, the first value becomes the accumulator and the accumulator function is not called for it; from the second value on, the function runs and its result is emitted. This is why an unseeded `scan` over objects with a numeric accumulator emits a raw object first - seed it whenever the types differ.
  • In RxJS, why does source$.pipe(toArray()) never emit for a stream of clicks?
    `toArray()` is built on `reduce`: it collects values and emits the array only when the source completes. A click stream never completes on its own, so the array is never emitted. Limit the source first - for example with `take(5)` - or use `scan` if you need the growing array as it builds.

saying these in an interview costs you the question

  • reduce emits the running total after each value, just like Array.reduce in a loop.
  • scan waits for the source to complete before emitting.
  • scan with a seed emits the seed immediately on subscribe.
  • Without a seed, scan calls the accumulator for the first value with undefined.
  • Mutating and returning the same accumulator object is fine for downstream change checks.