skip to content

In RxJS, why does pairwise() emit nothing for the first value, and how does startWith() change what it emits?

level: middleimportance: nice to knowfreq 28%

answer

  1. a pair needs two values
  2. [previous, current] tuples
  3. a synthetic first value
  4. emitted before the source

basics

~10 s

pairwise emits [previous, current] tuples, so it needs two values before its first emission. Putting startWith(x) before it supplies a synthetic first value, so the first real value produces [x, first] immediately.

solid answer

~30 s

`pairwise()` remembers the last value and, from the second value on, emits `[previous, current]`. The first value has no predecessor, so it produces no output; a source of N values yields N-1 pairs. `startWith(...values)` emits its arguments synchronously on subscribe, before subscribing to the source, so `startWith(0), pairwise()` turns a cart-total stream `10, 25` into `[0, 10], [10, 25]` - handy for showing "+10" on the first addition. Order matters: `startWith` after `pairwise` would inject a raw value into a stream of tuples. Passing a scheduler to `startWith` is deprecated.

code

ts · 15 lines
ts
import { Subject, map, pairwise, scan, startWith } from 'rxjs';

const addedAmounts$ = new Subject<number>();

const addedDelta$ = addedAmounts$.pipe(
  scan((total, amount) => total + amount, 0),
  startWith(0),
  pairwise(),
  map(([before, after]) => after - before),
);

addedDelta$.subscribe((d) => console.log(`+${d}`));

addedAmounts$.next(10); // +10
addedAmounts$.next(15); // +15

go deeper

for a junior

Recall that pairwise emits previous and current values together and that startWith prepends a value before the source.

for a middle

Explain why N values give N-1 pairs, that startWith emits synchronously on subscribe, and why startWith must come before pairwise to seed it.

for a senior

Choose domain-correct seeds, avoid adding a second startWith to a stream that already begins with one, and use pairwise deltas for change indicators without extra state.

for a principal

Judge when previous-versus-current comparisons belong in the stream and when they belong in a state layer that already tracks history.

## What pairwise emits `pairwise()` is a transformation operator that turns a stream of values into a stream of **consecutive pairs**. It stores the most recent value and, when the next arrives, emits a tuple `[previous, current]`, typed `[T, T]`. | Source values | `pairwise()` output | |---|---| | `a` | nothing | | `a, b` | `[a, b]` | | `a, b, c` | `[a, b]`, `[b, c]` | The first value has no predecessor, so it is only remembered. A source of **N** values therefore yields **N - 1** pairs, and a single-value source yields none. `pairwise` passes errors and completion through unchanged and does not emit a final pair on completion. Typical uses: - computing a **delta** between successive readings - a cart total, a price, a counter; - detecting **direction** - up or down, forward or back; - comparing the previous and current state of an object to decide what changed. ## What startWith does `startWith(...values)` prepends values to a stream. In RxJS 7 it is implemented by concatenating the given values with the source, which has two consequences: 1. the values are emitted **synchronously, on subscribe**, before the source is even subscribed; 2. they are emitted in argument order, and then the source's values follow. `endWith(...values)` is its mirror image, appending values after the source completes. ## Combining them Because `pairwise` swallows the first value, a UI that shows the change since the last update shows nothing for the first update. Seeding the stream with `startWith` fixes it: - `total$.pipe(startWith(0), pairwise())` over totals `10, 25` emits `[0, 10]` and `[10, 25]`; - mapping each pair to `curr - prev` gives `+10` and `+15`. The seed should be a real "previous" value for the domain - `0` for an empty cart, the persisted value for a restored session - otherwise the first delta is meaningless. **Order matters.** `pairwise(), startWith([0, 0])` would put a tuple at the front of the output, unrelated to the source; `startWith(0), pairwise()` gives `pairwise` a predecessor. And a stream that already begins with a seed - for example `scan(...)` followed by `startWith(0)` - must not get a second `startWith(0)` before `pairwise`, or the first pair becomes `[0, 0]`. ## The cart-total scenario A running cart total is built with `scan` over cart additions. The header wants to flash the amount just added: 1. `scan` produces the running total after each addition; 2. `startWith(0)` gives an initial total for an empty cart; 3. `pairwise()` turns totals into `[before, after]`; 4. `map(([before, after]) => after - before)` gives the amount added. With this ordering, the very first addition already produces a delta. ## Alternatives to pairwise - **`bufferCount(2, 1)`** also produces overlapping pairs, but it is not a drop-in replacement: when the source completes it flushes the still-open buffer, so the last emission is a one-element array such as `[25]`. - **`scan`** can carry the previous value explicitly - `scan((acc, curr) => ({ prev: acc.curr, curr }), { prev: 0, curr: 0 })` - which is useful when more than two values of history are needed. - For most delta and direction checks, `startWith` plus `pairwise` is the shortest correct form, and its `[T, T]` tuple type documents the intent. ## Details worth knowing - `pairwise` keeps **one** previous value per subscription; each subscriber gets its own pairing. - The tuple is a new array each time, so reference-based change checks see every pair as new. - `startWith(null)` and `startWith(undefined)` are valid and widen the type to include `null` or `undefined`. - Passing a scheduler as the last argument to `startWith` is deprecated since RxJS 6.5 and scheduled for removal in v8. ## Summary `pairwise` needs two values to form a pair, so the first is silent; `startWith` supplies a synthetic first value synchronously, and placed before `pairwise` it makes the first real value produce a pair.

  • In RxJS, what does of(5).pipe(pairwise()) emit?
    Nothing but completion. With a single value there is no previous value to pair with, and `pairwise` does not emit an incomplete pair when the source completes. Adding `startWith(0)` before it would produce `[0, 5]`.
  • In RxJS, when does startWith emit its values relative to subscribing to the source?
    Before it. `startWith` concatenates its values with the source, so on subscribe it emits them synchronously, in argument order, and only then subscribes to the source. That is why a UI bound to `source$.pipe(startWith(initial))` has a value immediately, even when the source is asynchronous.

pairwise is a turnstile counter that can only report 'how many since last time' once it has a previous reading; startWith is writing the opening reading on the clipboard before the doors open.

saying these in an interview costs you the question

  • pairwise emits [undefined, first] for the first value.
  • pairwise emits a final pair containing the last value when the source completes.
  • startWith emits its value after the source's first value arrives.
  • Placing startWith after pairwise gives pairwise a seed value.
  • Passing a scheduler to startWith is the current way to make its value asynchronous.