In RxJS, what does distinctUntilChanged() drop from a sensor stream, and why can the same reading still appear twice?
answer
- memory of one
- compares with the previous emission
- === by default
- distinct() remembers everything
basics
~20 sdistinctUntilChanged() drops a value only when it equals the last value it emitted, using === by default. It remembers one key, so 21, 21, 22, 21 becomes 21, 22, 21; distinct() is the operator that suppresses every earlier repeat.
solid answer
~40 s`distinctUntilChanged()` keeps exactly one piece of state: the key of the last value it let through. Each new value is compared with that key (`===` unless you pass a comparator); if they are equal the value is dropped, otherwise it is emitted and becomes the new reference. The first value is always emitted. Because the memory is only one value deep, a reading that returns after a different one passes again: `21, 21, 22, 21` emits `21, 22, 21`. That is exactly what you want for a sensor feed or a form value, where only a *change* matters. If you need to suppress any value ever seen, that is `distinct()`, which keeps a `Set` of every key and grows without bound on an endless stream unless you give it a `flushes` notifier.
code
ts · 10 linesimport { interval, map, distinctUntilChanged } from 'rxjs';
const readSensor = () => Math.round(20 + Math.random());
const temperature$ = interval(1000).pipe(
map(() => readSensor()),
distinctUntilChanged()
);
temperature$.subscribe(c => console.log('temperature changed to', c));go deeper
Recall that only consecutive duplicates are dropped, that the first value always passes, and give the 21, 21, 22, 21 example with its output.
Explain the single previousKey, the default === comparison, why objects defeat it, and when distinct() with its growing Set is the right tool instead.
Show where it belongs in a real pipeline: after reducing to a primitive, before costly work, and why distinct() on an endless stream is a memory leak without flushes.
Frame it as a contract decision: which layer guarantees change-only emission, and whether producers should emit primitives or stable references so consumers stay cheap.
## What the operator remembers `distinctUntilChanged` is a pipeable operator from the `rxjs` package (importable from `'rxjs'` since 7.2; the `'rxjs/operators'` path still works but is deprecated). It answers one question for every incoming value: **is this the same as the value I emitted last?** In RxJS 7.8 the implementation keeps two variables per subscription: - a `first` flag, so the very first value is **always emitted** — there is nothing to compare it with; - `previousKey`, the key of the **last emitted** value (by default the value itself). For each later value it calls the comparator — `(a, b) => a === b` unless you supply one — with `previousKey` and the new key. If the comparator returns `true` the values are considered equal and the new one is **dropped**; if it returns `false` the value is **emitted** and becomes the new `previousKey`. Errors and completion pass straight through, and nothing is emitted at completion. Two implementation details are worth knowing: - the state lives **per subscription**, so two subscribers to the same cold pipeline each get their own memory; - the key is updated **before** the value is emitted, so re-entrant code that pushes a value back into the source during emission is compared against the right reference (fixed in the RxJS 7.0 beta cycle). ## Walking a sensor stream Imagine a thermometer that reports every second, even when nothing changed. Downstream, a chart or an alerting rule only cares when the reading moves. | incoming | previousKey | equal? | emitted | |---|---|---|---| | 21 | (none) | first value | 21 | | 21 | 21 | yes | — | | 21 | 21 | yes | — | | 22 | 21 | no | 22 | | 22 | 22 | yes | — | | 21 | 22 | no | 21 | The final `21` is emitted even though `21` was seen before: the operator only compares **adjacent** emissions. That is the correct behaviour for a sensor — the temperature really did change back. ## distinctUntilChanged versus distinct | | `distinctUntilChanged()` | `distinct()` | |---|---|---| | compares against | the last emitted key only | every key ever emitted | | state | one value | a `Set` that keeps growing | | `21, 22, 21` | `21, 22, 21` | `21, 22` | | safe on an endless stream | yes | only with a `flushes` notifier that clears the set | | typical use | sensors, form values, store selections | de-duplicating ids in a finite batch | RxJS documents that `distinct` keeps its keys in a `Set` and offers an optional `flushes` observable to clear it; without one, a long-lived stream of unique values leaks memory. ## Where it fits in a pipeline 1. **Reduce to a primitive first** when you can. `map(r => r.celsius)` followed by `distinctUntilChanged()` compares numbers, which `===` handles correctly. 2. **Place it before the expensive work** — a re-render, an HTTP call, a recalculation — so repeated values never reach it. 3. **Keep it after operators that produce new values**, not before: filtering duplicates and then mapping to a rounded value can re-introduce adjacent duplicates. For objects, `===` compares **references**, so a producer that builds a fresh object per reading defeats the default comparison; the fix is a comparator, a key selector or `distinctUntilKeyChanged`, which is a separate concern. One more placement rule: when several subscribers share a sensor feed, de-duplicate **once**, upstream of the sharing point, rather than repeating the operator in every consumer. Each copy of the operator keeps its own memory, so duplicated operators cost memory and CPU without changing the result. ## Common misreadings - "It removes all duplicates" — it removes consecutive ones only. - "It swallows the first value" — the first value is always emitted. - "It waits for the stream to settle" — it has no notion of time; it decides synchronously per value. Waiting for a quiet period is `debounceTime`, a different operator. - "It emits the last value on completion" — completion is forwarded as-is. ```ts import { of, distinctUntilChanged, distinct } from 'rxjs'; const celsius$ = of(21, 21, 21, 22, 22, 21); celsius$.pipe(distinctUntilChanged()).subscribe(v => console.log('changed', v)); // changed 21, changed 22, changed 21 celsius$.pipe(distinct()).subscribe(v => console.log('unique', v)); // unique 21, unique 22 ``` In an Angular app the same operator commonly sits on a form control's `valueChanges` or on a selection from a service's state stream, so that a subscriber only reacts when the value actually differs from the last one it saw.
- What does distinctUntilChanged() do when the source errors or completes?It forwards both notifications unchanged. It holds no buffered value, so nothing extra is emitted at completion; the subscriber simply receives `complete` or `error` right after the last value that passed.
- Why prefer distinctUntilChanged() over distinct() on a long-lived sensor stream?`distinct()` stores every key it has emitted in a `Set`, so an endless stream of changing readings grows memory without bound unless a `flushes` observable clears it. It would also hide a legitimate return to an earlier value. `distinctUntilChanged()` keeps one key and reports every real change.
A departures board that only repaints a row when the new status differs from what it currently shows: "Delayed, Delayed, Boarding, Delayed" repaints three times, because it compares only with what is on the board right now.
saying these in an interview costs you the question
- distinctUntilChanged() removes every duplicate the stream has ever produced.
- The first value is dropped because there is nothing to compare it with.
- It compares objects by their contents out of the box.
- It waits for the stream to go quiet before emitting, like debounceTime.
- distinct() is safe on endless streams because it forgets old keys by itself.