In RxJS, when would you use withLatestFrom instead of combineLatest, and why can withLatestFrom silently drop source values?
answer
- who is allowed to trigger
- one driver, passive companions
- values before companions are ready
- completion follows the source
basics
~10 sUse withLatestFrom when only the source stream should trigger output and other streams just supply their latest value; source values that arrive before every other stream has emitted once are discarded, not queued.
solid answer
~40 s`combineLatest([a$, b$])` is symmetric: a new value on **any** source produces an emission. `source$.pipe(withLatestFrom(other$))` is asymmetric: only `source$` triggers, each emission is `[sourceValue, latestOther]`, and new values on `other$` only update the stored value. That fits 'on each Save click, send the current draft'. The catch: until **every** stream passed to `withLatestFrom` has emitted at least once, source values are **dropped** — no error, no buffering. A click before the draft's first value simply disappears. The result also completes when the source completes; the other streams completing changes nothing. Seeding the other stream — a `BehaviorSubject` or `startWith` — closes the gap.
code
ts · 12 linesimport { BehaviorSubject, Subject, withLatestFrom } from 'rxjs';
const saveClicks$ = new Subject<void>();
const draft$ = new BehaviorSubject<string>(''); // slot filled from the start
saveClicks$
.pipe(withLatestFrom(draft$))
.subscribe(([, draft]) => console.log('save', JSON.stringify(draft)));
saveClicks$.next(); // save "" (no longer dropped)
draft$.next('v1');
saveClicks$.next(); // save "v1"go deeper
Recall that withLatestFrom emits only when the source emits, pairing it with the latest value of the other stream.
Explain the asymmetric trigger, the dropped source values before every companion has emitted, companion-first subscription order and source-driven completion.
Recognise a save-on-every-keystroke bug from a symmetric join and a lost-first-click bug from an unseeded companion, and fix each by picking the right join or seeding state.
Decide which streams in a feature are triggers and which are state, and make state streams always hold a value so every consumer can read them without timing assumptions.
## Symmetric versus source-driven joins Both operators produce arrays that pair values from several streams, so they are easy to confuse. The difference is **who is allowed to cause an emission**. - **`combineLatest([a$, b$])`** — a static function — is **symmetric**. Once both have emitted, a new value from either one produces a new array. - **`source$.pipe(withLatestFrom(other$))`** — a pipeable operator — is **source-driven**. Only a value from `source$` produces output; a value from `other$` just replaces the stored latest value and produces nothing. `withLatestFrom` accepts several companion streams (`withLatestFrom(a$, b$)`), and emits `[sourceValue, latestA, latestB]`. ## Where each one fits | Situation | Operator | Why | |---|---|---| | Recompute a filtered list whenever the query or the sort changes | `combineLatest` | either input changing must refresh the output | | On each Save click, send the current form draft | `withLatestFrom` | typing must not save; only the click should | | On each keyboard shortcut, act on the currently selected item | `withLatestFrom` | changing the selection alone must do nothing | | Show a price that depends on quantity and currency | `combineLatest` | both inputs are equal partners | A common bug is using `combineLatest([saveClicks$, draft$])` for the save case: after the first click, **every keystroke** in the draft produces an emission, and the "save" fires on each one. ## How withLatestFrom actually runs 1. On subscription it subscribes to every **companion** stream first, then to the source. 2. Each companion's latest value is stored in a slot. 3. When the source emits, the operator checks whether every slot has been filled at least once. 4. If yes, it emits `[sourceValue, ...latestCompanions]`. 5. If no, the source value is **dropped**: nothing is emitted, nothing is queued for later and no error is raised. 6. The result completes when the **source** completes. A companion completing is ignored — its last value stays in its slot. 7. An error from the source or from any companion errors the result. Because companions are subscribed before the source, a companion that emits **synchronously** on subscription — a `BehaviorSubject`, `of(...)`, a replaying stream — fills its slot before the first source value can arrive. A companion that emits later (a request, a plain `Subject`, a user event) leaves a window in which source values vanish. ## The silent drop, and how to close it ```ts import { Subject, withLatestFrom } from 'rxjs'; const saveClicks$ = new Subject<void>(); const draft$ = new Subject<string>(); saveClicks$ .pipe(withLatestFrom(draft$)) .subscribe(([, draft]) => console.log('save', draft)); saveClicks$.next(); // dropped: draft$ has not emitted yet draft$.next('v1'); // stored, no output draft$.next('v2'); // stored, no output saveClicks$.next(); // save v2 ``` The first click produces nothing, and nothing in the console says why. Ways to close the gap: - Make the companion **stateful**: hold the draft in a `BehaviorSubject` seeded with an initial value, so the slot is filled from the start. - **Seed** the companion with `startWith(initialDraft)` when it is a plain stream. - If an early trigger must not be lost, withLatestFrom is the wrong tool: map each click to the draft stream's next value instead, which the flattening operators do. ## Other differences worth knowing - `withLatestFrom` has **no static form**; it only exists as an operator. `combineLatest` is the static function, with `combineLatestWith` as its pipeable form. - `withLatestFrom` does not wait for companions to complete, so it works with long-lived state streams, just as `combineLatest` does. - Neither operator re-emits the companion's value by itself: with `withLatestFrom`, an idle source means no output however often the companions change.
- Does withLatestFrom complete when the companion stream completes?No. Its result completes when the source completes. A companion that completes just keeps its last value in its slot and later source values keep pairing with it. If the companion completes before ever emitting, though, its slot can never fill, so every later source value is dropped until the source completes.
- Why does a BehaviorSubject companion never cause the drop, while a plain Subject can?`withLatestFrom` subscribes to its companions before the source. A `BehaviorSubject` emits its current value synchronously on subscription, so the slot is filled before any source value arrives. A plain `Subject` emits only on future `next()` calls, so a source value arriving first finds an empty slot and is discarded.
A photographer who only shoots when the button is pressed: whatever the scene looks like at that moment is in the photo, scene changes between presses produce no photos, and a press before the lens cap comes off yields nothing at all.
saying these in an interview costs you the question
- withLatestFrom emits whenever any of the combined streams emits
- withLatestFrom queues early source values until the other stream is ready
- withLatestFrom throws an error when the other stream has not emitted yet
- withLatestFrom completes as soon as the companion stream completes
- combineLatest and withLatestFrom are interchangeable; only the syntax differs