In RxJS, why does a scroll-position tracker using throttleTime(100) miss the final position, and what do its leading and trailing options change?
answer
- default config drops the end
- third argument, after the scheduler
- trailing emits the latest
- both false emits nothing
basics
~20 sthrottleTime defaults to { leading: true, trailing: false }: it emits each window's first value and drops the rest, so a scroll ending mid-window loses its final position. { leading: true, trailing: true }, passed third, also emits each window's latest value.
solid answer
~40 s`throttleTime(duration, scheduler?, config?)` takes its `ThrottleConfig` as the **third** argument, after the scheduler, so you write `throttleTime(100, asyncScheduler, { leading: true, trailing: true })`. The default is `{ leading: true, trailing: false }`: the first value passes, the window opens for 100 ms, and every value inside it is dropped — including the last scroll position. `trailing: true` makes the operator emit the most recent value when the window closes, and that emission opens a new window. `leading: false, trailing: true` delays even the first value to the window's end. `leading: false, trailing: false` emits nothing at all. With `trailing: true`, a value pending when the source completes is still emitted at the end of the window.
code
ts · 10 linesimport { fromEvent, map, throttleTime, asyncScheduler } from 'rxjs';
const scrollY$ = fromEvent(window, 'scroll').pipe(
map(() => window.scrollY),
throttleTime(100, asyncScheduler, { leading: true, trailing: true })
);
scrollY$.subscribe(y => updateProgressBar(y));
declare function updateProgressBar(y: number): void;go deeper
Recall the default leading true, trailing false and that it drops the last value inside a window.
Explain all four leading and trailing combinations, the third-argument position of the config, and how a trailing emission starts a new window.
Configure a scroll tracker that is both responsive and correct at rest, and reason about completion when trailing is on.
Weigh update frequency against rendering cost and correctness for scroll-driven UI, and standardise the configuration so features behave alike.
## The shape of the API In RxJS 7.8, `throttleTime` is declared as: ```ts throttleTime<T>(duration: number, scheduler: SchedulerLike = asyncScheduler, config?: ThrottleConfig) ``` `ThrottleConfig` has two optional booleans, `leading` and `trailing`. Internally `throttleTime` is `throttle(() => timer(duration, scheduler), config)`, and `throttle` reads the config as `leading = true, trailing = false` when a flag is missing. Two practical consequences: - The config is the **third** argument. `throttleTime(100, { trailing: true })` does not type-check, because the second slot is the scheduler; pass `asyncScheduler` (or `undefined`) first. - A **partial** config keeps the other default: `{ trailing: true }` still has `leading: true`. RxJS 7.8.1 fixed a bug in how default config values were handled, so spelling out both flags keeps the intent clear on any 7.x version. ## What each combination emits Consider scroll positions arriving every 30 ms, labelled by their arrival time (0, 30, 60 … 270), with `throttleTime(100, asyncScheduler, config)`: | `leading` | `trailing` | behaviour | final position 270 | |---|---|---|---| | `true` | `false` (default) | first value of each window, rest dropped | lost | | `true` | `true` | first value at once, latest value at each window end | emitted | | `false` | `true` | nothing at once, latest value at each window end | emitted | | `false` | `false` | a window starts, but nothing is ever sent | lost (nothing emits) | Tracing the default row: `0` is emitted and opens a window to 100; `30`, `60`, `90` are dropped; `120` is emitted and opens a window to 220; `240` is emitted; `270` falls inside that window and is dropped. With both edges on, the window ends emit `90`, `180` and finally `270`. The last row surprises people: with `leading: false` the first value only starts the window, and with `trailing: false` the window's end sends nothing, so the operator emits **nothing ever**. ## How the trailing edge works With `trailing: true`: 1. Values arriving during an open window overwrite a single pending slot. 2. When the window closes, the pending value (if any) is emitted. 3. That emission **immediately opens a new window**, so emissions stay at least `duration` apart. 4. If no value arrived during the window, nothing is emitted and the operator waits for the next value. For a scroll tracker with `{ leading: true, trailing: true }` this gives an immediate reaction when scrolling starts, at most one update per 100 ms during the scroll, and a final update carrying the resting position. ## Completion When the source completes: - with `trailing: true` and a pending value inside an open window, the operator waits for the window to close, emits that value, then completes; - otherwise it completes immediately and any values dropped inside the window stay dropped. A scroll stream from `fromEvent` never completes on its own, so for the tracker the window-end emission is what matters; the completion rule matters for finite sources and tests. ## Choosing the configuration - **Progress bar or sticky header:** `{ leading: true, trailing: true }` — responsive start, correct end. - **Analytics "user reached 50 %" events:** the default leading-only throttle is fine if an occasional missed final value is acceptable. - **Only the settled value matters:** that is no longer throttling; `debounceTime` or `auditTime` express it better. The general form, `throttle(durationSelector, config)`, accepts the same config and lets each window's length come from an observable chosen per value.
- Why does throttleTime(100, { trailing: true }) fail to compile?The second parameter of `throttleTime` is the scheduler, typed `SchedulerLike`; the `ThrottleConfig` is the third. Write `throttleTime(100, asyncScheduler, { trailing: true })`, or pass `undefined` for the scheduler to keep the default.
- With trailing enabled, how far apart can two emissions be?At least `duration`. A trailing emission opens a new window, so the next value can go out no earlier than one full duration later; the operator never emits twice inside one window.
saying these in an interview costs you the question
- throttleTime emits the last value of each window by default.
- The config object is throttleTime's second argument.
- leading: false with trailing: false means no throttling at all.
- A trailing emission lets the next value through immediately.
- Passing { trailing: true } turns the leading edge off.