In RxJS, why does from(fetch(url)) not send a new request per subscription, and how does defer() change that?
answer
- fetch runs when the expression runs
- one promise, many subscribers
- a factory called per subscribe
- iif is built on defer
basics
~20 sfetch(url) starts the request as soon as the expression is evaluated, and from() only adapts that one promise, so every subscriber gets the same response. defer(() => fetch(url)) calls the factory on each subscribe, sending one request per subscription and none before.
solid answer
~40 sIn `from(fetch(url))`, the `fetch` call runs when the line executes, before anyone subscribes, and `from` just wraps the single resulting promise. Every subscriber - and every resubscription, for example by a retry - reads that one settled result, and unsubscribing cannot cancel the request. `defer(factory)` stores a function instead and calls it **inside each subscribe**, converting whatever it returns with the same rules as `from`. So `defer(() => fetch(url))` sends nothing until subscribed and a fresh request per subscription. `iif(condition, a, b)` is `defer` underneath, so its condition is evaluated at subscribe time - but `a` and `b` are ordinary arguments, evaluated when `iif` is called. `ajax()` from `rxjs/ajax` is lazy by design and aborts its request on unsubscribe.
code
ts · 13 linesimport { defer, from } from 'rxjs';
const eager$ = from(fetch('/api/config')); // request already sent here
const lazy$ = defer(() => fetch('/api/config')); // nothing sent yet
eager$.subscribe(); // no new request
eager$.subscribe(); // no new request: same promise
lazy$.subscribe(); // request 1
lazy$.subscribe(); // request 2
const stamp$ = defer(async () => ({ at: Date.now(), body: await (await fetch('/api/ping')).text() }));
// each subscription calls the async factory oncego deeper
Recall that defer takes a function and calls it when someone subscribes, which makes promise-based work lazy.
Explain why from(fetch()) sends one request shared by all subscribers, how defer gives one request per subscription, and that iif is defer underneath.
Diagnose retries that never re-send, wasted iif branches and uncancellable requests, and choose between defer around a promise and an abortable source.
Set a convention that service methods return lazy Observables, never pre-started promises, so callers control when work starts and whether it can be cancelled.
## The eager promise problem A Promise-returning function such as `fetch(url)` does its work when it is **called**. In the expression `from(fetch(url))`, JavaScript evaluates the argument first, so the HTTP request is already on its way before `from` runs and long before anyone subscribes. RxJS's `from()` then adapts that single promise into an Observable: - every subscriber receives the **same** response, because there is only one promise; - **re-subscribing** - which is what retry operators do - reads the same settled promise again, so a failed request is never actually re-sent; - **unsubscribing** stops the value being delivered but cannot cancel the request, because the promise has no cancel; - a request is made even if nothing ever subscribes. None of this is a bug in `from`; it faithfully adapts what it is given. The fix is to delay creating the promise. ## What defer does `defer(observableFactory)` returns an Observable whose subscribe logic is, in RxJS 7's source, a single line: call `observableFactory()`, convert the result with the same rules as `from()`, and subscribe to it. Because the factory runs **inside subscribe**: 1. nothing happens until someone subscribes; 2. each subscription calls the factory again and gets its own fresh source; 3. resubscribing after an error runs the factory again, so a retry really does send a new request. The factory may return any `ObservableInput` - an Observable, a promise, an array, an iterable - so `defer(async () => { ... })` also works: each subscription calls the async function once. | Expression | Request starts | Requests for 3 subscribers | Retry re-sends | |---|---|---|---| | `from(fetch(url))` | when the line runs | 1 | no | | `defer(() => fetch(url))` | on each subscribe | 3 | yes | | `ajax(url)` (`rxjs/ajax`) | on each subscribe | 3 | yes | ## Other uses of defer The same pattern fixes anything that captures state too early: - **Timestamps or random values**: `of(Date.now())` captures the time when the line runs; `defer(() => of(Date.now()))` captures it per subscription. - **Reading storage or configuration**: `defer(() => of(localStorage.getItem(key)))` reads the current value each time instead of once at module load. - **Choosing a source at subscribe time**: pick one of several Observables based on current state. ## iif: a conditional defer `iif(condition, trueResult, falseResult)` is implemented as `defer(() => condition() ? trueResult : falseResult)`. Two consequences: - the **condition function** is evaluated per subscription, not when `iif` is called; - the **two results** are ordinary function arguments, evaluated when `iif` is called. If they are Observables, that is harmless - they are lazy. If they are promises, as in `iif(() => online, fetch(a), fetch(b))`, **both requests start immediately**, and the condition only chooses which result to read. When the branches have side effects, write `defer(() => online ? fetch(a) : fetch(b))` instead. ## ajax: a lazy, cancellable request source RxJS ships its own request function in the `rxjs/ajax` entry point. `ajax(url)` and `ajax.getJSON(url)` build an Observable that creates an `XMLHttpRequest` **on each subscribe** and, in its teardown, calls `xhr.abort()` if the request has not finished. A status of 400 or above arrives as an `AjaxError`. In Angular applications, `HttpClient` is the usual request API instead, and it is covered on its own. ## defer versus a function that returns an Observable A common alternative is a plain function, `load = () => from(fetch(url))`. It is lazy **per call**: each `load()` call starts a request, and whoever calls it decides when. `defer` moves that decision to **subscribe** time, which matters when the Observable itself is passed around: - a single `config$ = defer(...)` can be stored in a field and subscribed many times, each time with a fresh request; - operators that resubscribe, such as retry and repeat, re-run the work; - nothing runs if the stream is built but never subscribed, for example when a condition later skips it. ## How to spot the bug in review - A Promise-returning call written directly inside `from(...)` at module or service-construction level. - `iif` whose branches are promises or function calls with side effects. - A retry around a promise-backed Observable that "never retries" in the network log. The remedy in each case is the same: move the call into a factory with `defer`, or use a source that is already lazy.
- In RxJS, what is wrong with iif(() => isOnline, fetch('/a'), fetch('/b'))?Both `fetch` calls run when `iif` is called, because they are ordinary arguments; the condition only decides which settled result the subscriber reads. The unused request is wasted and may have side effects. Wrap the choice in `defer(() => isOnline ? fetch('/a') : fetch('/b'))` so only the chosen request is sent, at subscribe time.
- In RxJS, does unsubscribing from from(fetch(url)) cancel the HTTP request?No. The Observable adapts a promise that is already running, and a promise has no cancel, so unsubscribing only stops the value reaching the subscriber. Cancelling needs a source whose teardown aborts the request, such as `ajax()` from `rxjs/ajax`, which calls `xhr.abort()` on unsubscribe if the request has not finished.
from(fetch(url)) is a photocopy of a letter already posted: everyone gets a copy of the same reply. defer(() => fetch(url)) is a stamped, addressed envelope handed to each subscriber, who posts their own letter when they subscribe.
saying these in an interview costs you the question
- from(fetch(url)) waits for a subscriber before sending the request.
- Retrying a from(promise) Observable sends the request again.
- iif only evaluates the argument expression of the branch the condition selects.
- defer's factory runs once and its result is shared by every subscriber.
- Unsubscribing from from(fetch(url)) aborts the HTTP request.